/**
 * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { memo, ReactElement, useCallback } from "react"

import {
  LABEL_PLACEMENT,
  STYLE_TYPE,
  Checkbox as UICheckbox,
} from "baseui/checkbox"

import { Checkbox as CheckboxProto } from "@streamlit/protobuf"

import StreamlitMarkdown from "~lib/components/shared/StreamlitMarkdown"
import { Placement } from "~lib/components/shared/Tooltip"
import { WidgetLabelHelpIconInline } from "~lib/components/widgets/BaseWidget"
import {
  useBasicWidgetState,
  ValueWithSource,
} from "~lib/hooks/useBasicWidgetState"
import { useEmotionTheme } from "~lib/hooks/useEmotionTheme"
import { hasLightBackgroundColor } from "~lib/theme"
import { getFocusBoxShadow } from "~lib/theme/utils"
import { labelVisibilityProtoValueToEnum } from "~lib/util/utils"
import { WidgetStateManager } from "~lib/WidgetStateManager"

import { StyledCheckbox, StyledContent } from "./styled-components"

export interface Props {
  disabled: boolean
  element: CheckboxProto
  widgetMgr: WidgetStateManager
  fragmentId?: string
}

function Checkbox({
  element,
  disabled,
  widgetMgr,
  fragmentId,
}: Readonly<Props>): ReactElement {
  const [value, setValueWithSource] = useBasicWidgetState<
    boolean,
    CheckboxProto
  >({
    getStateFromWidgetMgr,
    getDefaultStateFromProto,
    getCurrStateFromProto,
    updateWidgetMgrState,
    element,
    widgetMgr,
    fragmentId,
  })

  const onChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>): void => {
      setValueWithSource({ value: e.target.checked, fromUi: true })
    },
    // ESLint complains if we remove this unnecessary dep.
    [setValueWithSource]
  )

  const theme = useEmotionTheme()
  const { colors, spacing, sizes } = theme

  const lightTheme = hasLightBackgroundColor(theme)

  const color = disabled ? colors.fadedText40 : colors.bodyText

  return (
    <StyledCheckbox className="row-widget stCheckbox" data-testid="stCheckbox">
      <UICheckbox
        checked={value}
        disabled={disabled}
        onChange={onChange}
        aria-label={element.label}
        checkmarkType={
          element.type === CheckboxProto.StyleType.TOGGLE
            ? STYLE_TYPE.toggle
            : STYLE_TYPE.default
        }
        labelPlacement={LABEL_PLACEMENT.right}
        overrides={{
          Root: {
            style: ({ $isFocusVisible }: { $isFocusVisible: boolean }) => ({
              marginBottom: spacing.none,
              marginTop: spacing.none,
              backgroundColor: $isFocusVisible ? colors.darkenedBgMix25 : "",
              display: "flex",
              alignItems: "start",
            }),
          },
          Toggle: {
            style: ({ $checked }: { $checked: boolean }) => {
              let backgroundColor = lightTheme
                ? colors.bgColor
                : colors.bodyText

              if (disabled) {
                backgroundColor = lightTheme ? colors.gray70 : colors.gray90
              }
              return {
                width: `calc(${sizes.checkbox} - ${theme.spacing.twoXS})`,
                height: `calc(${sizes.checkbox} - ${theme.spacing.twoXS})`,
                transform: $checked ? `translateX(${sizes.checkbox})` : "",
                backgroundColor,
                boxShadow: "",
              }
            },
          },
          ToggleTrack: {
            style: ({
              $checked,
              $isHovered,
            }: {
              $checked: boolean
              $isHovered: boolean
            }) => {
              let backgroundColor = colors.borderColor

              if ($isHovered && !disabled) {
                backgroundColor = colors.darkenedBgMix15
              }

              if ($checked && !disabled) {
                backgroundColor = colors.primary
              }

              return {
                marginRight: 0,
                marginLeft: 0,
                marginBottom: 0,
                marginTop: theme.spacing.twoXS,
                paddingLeft: theme.spacing.threeXS,
                paddingRight: theme.spacing.threeXS,
                width: `calc(2 * ${sizes.checkbox})`,
                minWidth: `calc(2 * ${sizes.checkbox})`,
                height: sizes.checkbox,
                minHeight: sizes.checkbox,
                borderBottomLeftRadius: theme.radii.full,
                borderTopLeftRadius: theme.radii.full,
                borderBottomRightRadius: theme.radii.full,
                borderTopRightRadius: theme.radii.full,
                backgroundColor,
              }
            },
          },
          Checkmark: {
            style: ({
              $isFocusVisible,
              $checked,
            }: {
              $isFocusVisible: boolean
              $checked: boolean
            }) => {
              const borderColor =
                $checked && !disabled ? colors.primary : colors.borderColor

              return {
                outline: 0,
                width: sizes.checkbox,
                height: sizes.checkbox,
                marginTop: theme.spacing.twoXS,
                marginLeft: 0,
                marginBottom: 0,
                boxShadow:
                  $isFocusVisible && $checked
                    ? getFocusBoxShadow(colors.primary)
                    : "",
                // This is painfully verbose, but baseweb seems to internally
                // use the long-hand version, which means we can't use the
                // shorthand names here as if we do we'll end up with warn
                // logs spamming us every time a checkbox is rendered.
                borderLeftWidth: sizes.borderWidth,
                borderRightWidth: sizes.borderWidth,
                borderTopWidth: sizes.borderWidth,
                borderBottomWidth: sizes.borderWidth,
                borderLeftColor: borderColor,
                borderRightColor: borderColor,
                borderTopColor: borderColor,
                borderBottomColor: borderColor,
              }
            },
          },
          Label: {
            style: {
              lineHeight: theme.lineHeights.small,
              paddingLeft: theme.spacing.sm,
              position: "relative",
              color,
            },
          },
        }}
      >
        <StyledContent
          visibility={labelVisibilityProtoValueToEnum(
            element.labelVisibility?.value
          )}
          data-testid="stWidgetLabel"
        >
          <StreamlitMarkdown
            source={element.label}
            allowHTML={false}
            isLabel
            largerLabel
          />
          {element.help && (
            <WidgetLabelHelpIconInline
              content={element.help}
              placement={Placement.TOP_RIGHT}
              label={element.label}
            />
          )}
        </StyledContent>
      </UICheckbox>
    </StyledCheckbox>
  )
}

function getStateFromWidgetMgr(
  widgetMgr: WidgetStateManager,
  element: CheckboxProto
): boolean | undefined {
  return widgetMgr.getBoolValue(element)
}

function getDefaultStateFromProto(element: CheckboxProto): boolean {
  return element.default ?? null
}

function getCurrStateFromProto(element: CheckboxProto): boolean {
  return element.value ?? null
}

function updateWidgetMgrState(
  element: CheckboxProto,
  widgetMgr: WidgetStateManager,
  vws: ValueWithSource<boolean>,
  fragmentId?: string
): void {
  widgetMgr.setBoolValue(
    element,
    vws.value,
    { fromUi: vws.fromUi },
    fragmentId
  )
}

export default memo(Checkbox)
