import { useEffect, useState, useRef } from "react";
import marioImage from "./assets/mario.png";
import useConvolutionProcessing from "./useConvolutionProcessing.ts";
import { Button, Card, Dropdown, Radio } from "@elvis/ui";

import { 
  DEFAULT_COLOR_KERNEL, 
  DEFAULT_GRAY_KERNEL,
  GRAY_KERNEL_PRESETS,
  COLOR_KERNEL_PRESETS,
} from "./kernels.ts";

const DEFAULT_IMAGE: string = marioImage;
const MIN_KERNEL_SIZE = 1;
const MAX_KERNEL_SIZE = 20;

export default function ConvolutionVisualizer() {
  const [rawInputImage, setRawInputImage] = useState<string>(DEFAULT_IMAGE);
  const [useColor, setUseColor] = useState<boolean>(true);
  const [colorKernel, setColorKernel] = useState<number[][][]>(DEFAULT_COLOR_KERNEL);
  const [grayscaleKernel, setGrayscaleKernel] = useState<number[][]>(DEFAULT_GRAY_KERNEL);
  const kernel = useColor ? colorKernel : grayscaleKernel;

  const [inputImage, outputImage] = useConvolutionProcessing(rawInputImage, kernel);

  return (
    <div className="grid grid-cols-2 gap-8 h-full min-h-0">
      <InputOutputViewer 
        input={inputImage}
        output={outputImage}
      />

      <div className="flex flex-col gap-8 h-full min-h-0">
        <ImageControls
          setImage={setRawInputImage}
          useColor={useColor}
          setUseColor={setUseColor}
        />
        <KernelEditor
          useColor={useColor}
          colorKernel={colorKernel}
          setColorKernel={setColorKernel}
          grayscaleKernel={grayscaleKernel}
          setGrayscaleKernel={setGrayscaleKernel}
        />
      </div>
    </div>
  );
}

type InputOutputViewerProps = {
  input: string | null;
  output: string | null;
}
function InputOutputViewer({ input, output }: InputOutputViewerProps) {
  return (
    <Card className="grid grid-rows-2 h-full min-h-0">

      <div className="flex flex-col items-center justify-center min-h-0">
        <h2 className="text-lg font-bold mb-2">Input Image</h2>
        {input ? (
          <img src={input} alt="Input" className="flex-1 max-w-full max-h-full min-h-0" />
        ) : (
          <p>Loading...</p>
        )}
      </div>

      <div className="flex flex-col items-center justify-center min-h-0">
        <h2 className="text-lg font-bold mb-2">Output Image</h2>
        {output ? (
          <img src={output} alt="Output" className="flex-1 max-w-full max-h-full min-h-0" />
        ) : (
          <p>Processing...</p>
        )}
      </div>

    </Card>
  );
}

type ImageControlsProps = {
  setImage: (imageUrl: string) => void;
  useColor: boolean;
  setUseColor: (useColor: boolean) => void;
}
function ImageControls({ setImage, useColor, setUseColor }: ImageControlsProps) {
  const imageFileInputRef = useRef<HTMLInputElement | null>(null);

  return (
    <Card className="flex justify-evenly items-center p-4 gap-4">
      <input
        ref={imageFileInputRef}
        type="file"
        accept="image/*"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (!file) return;

          const reader = new FileReader();
          reader.onload = () => {
            const result = reader.result;
            if (typeof result !== "string") {
              return;
            }
            setImage(result);
          };
          reader.readAsDataURL(file);
        }}
        style={{ display: "none" }}
      />
      <Button
        label="Upload Image"
        onClick={() => imageFileInputRef.current?.click()}
      />
      <Radio
        label="Color option"
        options={["Grayscale", "Color"] as const}
        activeOption={useColor ? "Color" : "Grayscale"}
        onChange={(option) => setUseColor(option === "Color")}
      />
    </Card>
  );
}

type KernelEditorProps = {
  useColor: boolean;
  colorKernel: number[][][];
  setColorKernel: (kernel: number[][][]) => void;
  grayscaleKernel: number[][];
  setGrayscaleKernel: (kernel: number[][]) => void;
}
function KernelEditor({
  useColor,
  colorKernel,
  setColorKernel,
  grayscaleKernel,
  setGrayscaleKernel,
}: KernelEditorProps) {
  const colorPresetNames = Object.keys(COLOR_KERNEL_PRESETS) as string[];
  const grayPresetNames = Object.keys(GRAY_KERNEL_PRESETS) as string[];
  const [selectedChannel, setSelectedChannel] = useState<number>(0);
  const [selectedColorPreset, setSelectedColorPreset] = useState<string>(colorPresetNames[0]);
  const [selectedGrayPreset, setSelectedGrayPreset] = useState<string>(grayPresetNames[0]);
  const [loadedColorPreset, setLoadedColorPreset] = useState<string | null>("Laplacian");
  const [loadedGrayPreset, setLoadedGrayPreset] = useState<string | null>("Laplacian");

  function colorKernelToRepr(nextKernel: number[][][]): string[][][] {
    return nextKernel.map((channel) =>
      channel.map((row) => row.map((value) => value.toString())),
    );
  }

  function grayKernelToRepr(nextKernel: number[][]): string[][] {
    return nextKernel.map((row) => row.map((value) => value.toString()));
  }

  const [draftColorKernel, setDraftColorKernel] = useState<string[][][]>(colorKernelToRepr(colorKernel));
  const [draftGrayKernel, setDraftGrayKernel] = useState<string[][]>(grayKernelToRepr(grayscaleKernel));

  useEffect(() => {
    setDraftColorKernel(colorKernelToRepr(colorKernel));
  }, [colorKernel]);

  useEffect(() => {
    setDraftGrayKernel(grayKernelToRepr(grayscaleKernel));
  }, [grayscaleKernel]);

  useEffect(() => {
    setSelectedChannel(0);
  }, [useColor]);

  function parseCell(cell: string): number | null {
    const trimmed = cell.trim();
    if (trimmed === "") return null;
    const parsed = Number(trimmed);
    return Number.isFinite(parsed) ? parsed : null;
  }

  function cloneColorKernel(nextKernel: number[][][]): number[][][] {
    return nextKernel.map((channel) => channel.map((row) => [...row]));
  }

  function cloneGrayKernel(nextKernel: number[][]): number[][] {
    return nextKernel.map((row) => [...row]);
  }

  function handleLoadPreset() {
    if (useColor) {
      const presetKernel = cloneColorKernel(
        COLOR_KERNEL_PRESETS[selectedColorPreset as keyof typeof COLOR_KERNEL_PRESETS],
      );
      setDraftColorKernel(colorKernelToRepr(presetKernel));
      setColorKernel(presetKernel);
      setLoadedColorPreset(selectedColorPreset);
      setSelectedChannel(0);
      return;
    }

    const presetKernel = cloneGrayKernel(
      GRAY_KERNEL_PRESETS[selectedGrayPreset as keyof typeof GRAY_KERNEL_PRESETS],
    );
    setDraftGrayKernel(grayKernelToRepr(presetKernel));
    setGrayscaleKernel(presetKernel);
    setLoadedGrayPreset(selectedGrayPreset);
  }

  function kernelsEqual2D(a: number[][], b: number[][]): boolean {
    if (a.length !== b.length) return false;
    if ((a[0]?.length ?? 0) !== (b[0]?.length ?? 0)) return false;
    return a.every((row, rowIndex) =>
      row.every((value, colIndex) => value === b[rowIndex][colIndex]),
    );
  }

  function kernelsEqual3D(a: number[][][], b: number[][][]): boolean {
    if (a.length !== b.length) return false;
    return a.every((channel, channelIndex) => kernelsEqual2D(channel, b[channelIndex]));
  }

  function handleKernelChange(channel: number, row: number, col: number, value: string) {
    if (useColor) {
      const nextDraft = draftColorKernel.map((c) => c.map((r) => [...r]));
      nextDraft[channel][row][col] = value;
      setDraftColorKernel(nextDraft);

      const parsedKernel = nextDraft.map((c) => c.map((r) => r.map(parseCell)));
      const isValid = parsedKernel.every((c) => c.every((r) => r.every((v) => v !== null)));
      if (isValid) {
        setColorKernel(parsedKernel as number[][][]);
      }
      return;
    }

    const nextDraft = draftGrayKernel.map((r) => [...r]);
    nextDraft[row][col] = value;
    setDraftGrayKernel(nextDraft);

    const parsedKernel = nextDraft.map((r) => r.map(parseCell));
    const isValid = parsedKernel.every((r) => r.every((v) => v !== null));
    if (isValid) {
      setGrayscaleKernel(parsedKernel as number[][]);
    }
  }

  function handleSizeChange(newWidth: number, newHeight: number) {
    const clampedWidth = Math.min(MAX_KERNEL_SIZE, Math.max(MIN_KERNEL_SIZE, newWidth));
    const clampedHeight = Math.min(MAX_KERNEL_SIZE, Math.max(MIN_KERNEL_SIZE, newHeight));

    function resizeMatrix(matrix: string[][], width: number, height: number): string[][] {
      return Array.from({ length: height }, (_, rowIndex) =>
        Array.from({ length: width }, (_, colIndex) => matrix[rowIndex]?.[colIndex] ?? "0"),
      );
    }

    if (useColor) {
      const resizedDraft = draftColorKernel.map((channelMatrix) =>
        resizeMatrix(channelMatrix, clampedWidth, clampedHeight),
      );
      setDraftColorKernel(resizedDraft);
      setColorKernel(
        resizedDraft.map((channelMatrix) =>
          channelMatrix.map((row) => row.map((cell) => parseCell(cell) ?? 0)),
        ),
      );
      return;
    }

    const resizedDraft = resizeMatrix(draftGrayKernel, clampedWidth, clampedHeight);
    setDraftGrayKernel(resizedDraft);
    setGrayscaleKernel(resizedDraft.map((row) => row.map((cell) => parseCell(cell) ?? 0)));
  }

  const channelLabels = ["R", "G", "B"];
  const activeMatrix = useColor
    ? draftColorKernel[selectedChannel]
    : draftGrayKernel;
  const currentHeight = activeMatrix.length;
  const currentWidth = activeMatrix[0]?.length ?? 0;
  const isModified = useColor
    ? loadedColorPreset !== null
      && !kernelsEqual3D(
        colorKernel,
        COLOR_KERNEL_PRESETS[loadedColorPreset as keyof typeof COLOR_KERNEL_PRESETS],
      )
    : loadedGrayPreset !== null
      && !kernelsEqual2D(
        grayscaleKernel,
        GRAY_KERNEL_PRESETS[loadedGrayPreset as keyof typeof GRAY_KERNEL_PRESETS],
      );
  const loadedPresetLabel = useColor ? loadedColorPreset : loadedGrayPreset;

  return (
    <Card className="flex flex-col items-center w-full min-h-0 h-full min-h-0 overflow-auto">
      <div className="flex items-end gap-2 mb-3">
        <Dropdown
          label="Kernel Preset"
          options={useColor ? colorPresetNames : grayPresetNames}
          activeOption={useColor ? selectedColorPreset : selectedGrayPreset}
          onChange={(option) => {
            if (useColor) {
              setSelectedColorPreset(option);
              return;
            }
            setSelectedGrayPreset(option);
          }}
        />
        <Button label="Load preset" onClick={handleLoadPreset} />
      </div>
      {loadedPresetLabel && (
        <div className="text-sm mb-3">
          <span>Loaded: {loadedPresetLabel}</span>
          {isModified && <span className="text-orange-700 ml-2">(modified)</span>}
        </div>
      )}
      { useColor && (
        <div className="flex gap-2 mb-3">
          {channelLabels.map((label, index) => (
            <button
              key={label}
              type="button"
              className={`px-3 py-1 text-sm rounded border ${
                selectedChannel === index ? "bg-orange-200 border-orange-300" : "bg-white border-gray-300"
              }`}
              onClick={() => setSelectedChannel(index)}
            >
              {label}
            </button>
          ))}
        </div>
      )}
      <div className="flex flex-col gap-4 mb-3">
        <div className="flex items-center gap-2">
          <span className="text-sm">Width</span>
          <button
            type="button"
            className="px-2 py-1 text-sm rounded border border-gray-300"
            onClick={() => handleSizeChange(currentWidth - 1, currentHeight)}
          >
            -
          </button>
          <span className="text-sm w-6 text-center">{currentWidth}</span>
          <button
            type="button"
            className="px-2 py-1 text-sm rounded border border-gray-300"
            onClick={() => handleSizeChange(currentWidth + 1, currentHeight)}
          >
            +
          </button>
        </div>
        <div className="flex items-center gap-2">
          <span className="text-sm">Height</span>
          <button
            type="button"
            className="px-2 py-1 text-sm rounded border border-gray-300"
            onClick={() => handleSizeChange(currentWidth, currentHeight - 1)}
          >
            -
          </button>
          <span className="text-sm w-6 text-center">{currentHeight}</span>
          <button
            type="button"
            className="px-2 py-1 text-sm rounded border border-gray-300"
            onClick={() => handleSizeChange(currentWidth, currentHeight + 1)}
          >
            +
          </button>
        </div>
      </div>
      <div className="flex flex-col gap-2">
        {activeMatrix.map((row, rowIndex) => (
          <div key={rowIndex} className="flex gap-2">
            {row.map((cellValue, colIndex) => (
              <input
                key={`${rowIndex}-${colIndex}`}
                type="text"
                className="w-14 px-2 py-1 border border-gray-300 rounded text-sm"
                value={cellValue}
                onChange={(event) =>
                  handleKernelChange(
                    useColor ? selectedChannel : 0,
                    rowIndex,
                    colIndex,
                    event.target.value,
                  )
                }
              />
            ))}
          </div>
        ))}
      </div>
    </Card>
  );
}
