import {
  createEffect,
  createMemo,
  createSignal,
  Match,
  on,
  Show,
  Switch,
} from "solid-js";
import RemotesService from "../services/remotes-service.js";
import EventEmitter from "../tools/event-emitter.js";
import ModalHandler from "./modal-handler.js";
import Modal from "./modal.jsx";
import Command from "../data/command.js";

const eventEmitter = new EventEmitter();
const COMMANDS_IMPORTED_EVENT = "success";

const ENTER_CSV_STEP = 1;
const MAP_FIELDS_STEP = 2;
const MAP_VALUES_STEP = 3;
const CONFIRM_STEP = 4;
const TOTAL_STEPS = 4;

const PROTOCOL_FIELD = "protocol";
const COMMAND_NUMBER_FIELD = "commandNumber";
const DEVICE_FIELD = "device";
const COMMAND_TYPE_FIELD = "commandType";
const TITLE_FIELD = "title";
let FieldTitles = {};
FieldTitles[PROTOCOL_FIELD] = "Protocol";
FieldTitles[COMMAND_NUMBER_FIELD] = "Command";
FieldTitles[DEVICE_FIELD] = "Device";
FieldTitles[COMMAND_TYPE_FIELD] = "Type";
FieldTitles[TITLE_FIELD] = "Title";

const CommandFieldsRequiringValueMapping = ["protocol", "commandType"];
const commandTypeFuse = new Fuse(
  Object.values(Command.TYPES).map((type) => Command.getTypeString(type))
);
const protocolsFuse = new Fuse(
  Object.values(Command.PROTOCOLS).map((protocol) =>
    Command.getProtocolString(protocol)
  )
);

function ImportCommandsModal(props) {
  const [csvString, setCsvString] = createSignal("");
  const [currentStep, setCurrentStep] = createSignal(ENTER_CSV_STEP);
  const [error, setError] = createSignal("");
  const [csvArray, setCsvArray] = createSignal([]);
  const [commands, setCommands] = createSignal([]);
  const [fieldMapping, setFieldMapping] = createSignal(
    {},
    { equals: () => false }
  );
  const [isComputingValueMapping, setComputingValueMapping] =
    createSignal(false);
  const [valueMapping, setValueMapping] = createSignal(
    (() => {
      let mapping = {};
      mapping[PROTOCOL_FIELD] = {};
      mapping[COMMAND_TYPE_FIELD] = {};
      return mapping;
    })(),
    { equals: () => false }
  );

  const canMakeNextStep = createMemo(() => {
    switch (currentStep()) {
      case ENTER_CSV_STEP:
        return csvString() !== "";
      default:
        return true;
    }
  });
  const fields = createMemo(() =>
    Object.keys(csvArray().find(() => true) || {})
  );

  createEffect(() => {
    switch (currentStep()) {
      case MAP_VALUES_STEP:
        onMapValuesStep();
        break;
    }
  });

  function handleNextStep() {
    switch (currentStep()) {
      case ENTER_CSV_STEP:
        handleEnterCsvStep();
        break;
      case MAP_FIELDS_STEP:
        handleMapFieldsStep();
        break;
      case MAP_VALUES_STEP:
        handleMapValuesStep();
        break;
      case CONFIRM_STEP:
        handleImportCommands();
        break;
      default:
        nextStep();
    }
  }

  async function handleImportCommands() {
    let newCommands = [];
    try {
      newCommands = await RemotesService.createCommands(commands());
    } catch (e) {
      setError(e.message);
      return;
    }
    resetFields();
    ImportCommandsModal.Handler.hide();
    eventEmitter.dispatchEvent(COMMANDS_IMPORTED_EVENT, newCommands);
  }

  function EnterCsvStep() {
    return (
      <div class="mb-3">
        <label for="csvTextarea" class="form-label">
          Enter CSV:
        </label>
        <textarea
          class="form-control"
          id="csvTextarea"
          rows="10"
          value={csvString()}
          onInput={(e) => setCsvString(e.target.value)}
        ></textarea>
      </div>
    );
  }

  function handleEnterCsvStep() {
    if (!csvString()) {
      setError("Please enter a CSV string.");
      return;
    }
    let result = Papa.parse(csvString(), { header: true });
    if (result.errors.length > 0) {
      setError(result.errors[0].message);
      return;
    }
    let csvArray = result.data;
    setCsvArray(csvArray);
    nextStep();
  }

  function MapFieldsStep() {
    function setMapping(field, commandField) {
      let mapping = fieldMapping();
      mapping[field] = commandField;
      setFieldMapping(mapping);
    }
    return (
      <div class="mb-3">
        <div>Map Fields:</div>
        <div class="d-flex flex-column">
          {fields().map((field) => (
            <div class="d-flex flex-row align-items-center justify-content-center text-end mb-2">
              <div class="col-sm-4">
                <div>{field}</div>
                <div class="fw-light lh-1">(e.g. {csvArray()[0][field]})</div>
              </div>
              <div style="font-size: 1.5rem; line-height: 1;">
                <i class="bi bi-arrow-right-short"></i>
              </div>
              <div class="col-sm-4">
                <select
                  class="form-select"
                  onChange={(e) => setMapping(field, e.target.value)}
                >
                  <option value="" selected>
                    Please select
                  </option>
                  {Object.keys(FieldTitles).map((commandField) => (
                    <option
                      value={commandField}
                      selected={fieldMapping()[field] === commandField}
                    >
                      {FieldTitles[commandField]}
                    </option>
                  ))}
                </select>
              </div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  function handleMapFieldsStep() {
    let usedCommandFields = [];
    let mapping = fieldMapping();
    for (let field in mapping) {
      if (usedCommandFields.includes(mapping[field])) {
        setError("Duplicate mapping found.");
        return false;
      }
      usedCommandFields.push(mapping[field]);
    }
    nextStep();
  }

  function MapValuesStep() {
    function setMapping(field, value, commandValue) {
      let mapping = valueMapping();
      if (!mapping[field]) mapping[field] = {};
      mapping[field][value] = commandValue;
      setValueMapping(mapping);
    }
    return (
      <div class="mb-3">
        <div>Map Values:</div>
        {Object.keys(fieldMapping())
          .filter((field) =>
            CommandFieldsRequiringValueMapping.includes(fieldMapping()[field])
          )
          .map((csvField) => {
            let field = fieldMapping()[csvField];
            return (
              <div>
                <div>{FieldTitles[field]}:</div>
                <div class="d-flex flex-column">
                  {csvArray()
                    .map((row) => row[csvField])
                    .filter(
                      (value, index, array) => array.indexOf(value) === index
                    )
                    .map((value) => (
                      <div class="d-flex flex-row align-items-center justify-content-center text-end mb-2">
                        <div class="col-sm-4">
                          <div>{value}</div>
                        </div>
                        <div style="font-size: 1.5rem; line-height: 1;">
                          <i class="bi bi-arrow-right-short"></i>
                        </div>
                        <div class="col-sm-4">
                          <Switch>
                            <Match when={field === "protocol"}>
                              <select
                                class="form-select"
                                onChange={(e) =>
                                  setMapping(field, value, e.target.value)
                                }
                              >
                                <option value="" selected>
                                  Please select
                                </option>
                                {Object.values(Command.PROTOCOLS).map(
                                  (protocol) => (
                                    <option
                                      value={protocol}
                                      selected={
                                        valueMapping()[PROTOCOL_FIELD][
                                          value
                                        ] === protocol
                                      }
                                    >
                                      {Command.getProtocolString(protocol)}
                                    </option>
                                  )
                                )}
                              </select>
                            </Match>
                            <Match when={field === "commandType"}>
                              <select
                                class="form-select"
                                onChange={(e) =>
                                  setMapping(field, value, e.target.value)
                                }
                              >
                                <option value="" selected>
                                  Please select
                                </option>
                                {Object.values(Command.TYPES).map(
                                  (commandType) => (
                                    <option
                                      value={commandType}
                                      selected={
                                        valueMapping()[COMMAND_TYPE_FIELD][
                                          value
                                        ] === commandType
                                      }
                                    >
                                      {Command.getTypeString(commandType)}
                                    </option>
                                  )
                                )}
                              </select>
                            </Match>
                          </Switch>
                        </div>
                      </div>
                    ))}
                </div>
              </div>
            );
          })}
      </div>
    );
  }

  function onMapValuesStep() {
    setComputingValueMapping(true);
    (async () => {
      let valueMapping = {};
      valueMapping[PROTOCOL_FIELD] = {};
      valueMapping[COMMAND_TYPE_FIELD] = {};
      let protocolField = Object.keys(fieldMapping()).find(
        (field) => fieldMapping()[field] === PROTOCOL_FIELD
      );
      let commandTypeField = Object.keys(fieldMapping()).find(
        (field) => fieldMapping()[field] === COMMAND_TYPE_FIELD
      );
      csvArray().forEach((row) => {
        let protocolValue = row[protocolField];
        let commandTypeValue = row[commandTypeField];
        if (!valueMapping[PROTOCOL_FIELD][protocolValue]) {
          let result = protocolsFuse.search(protocolValue).shift();
          if (result) {
            let protocol = Object.values(Command.PROTOCOLS).find(
              (protocol) => Command.getProtocolString(protocol) === result.item
            );
            valueMapping[PROTOCOL_FIELD][protocolValue] = protocol;
          }
        }
        if (!valueMapping[COMMAND_TYPE_FIELD][commandTypeValue]) {
          let result = commandTypeFuse.search(commandTypeValue).shift();
          if (result) {
            let commandType = Object.values(Command.TYPES).find(
              (commandType) =>
                Command.getTypeString(commandType) === result.item
            );
            valueMapping[COMMAND_TYPE_FIELD][commandTypeValue] = commandType;
          }
        }
      });
      setValueMapping(valueMapping);
      setComputingValueMapping(false);
    })();
  }

  function handleMapValuesStep() {
    let protocolField = Object.keys(fieldMapping()).find(
      (field) => fieldMapping()[field] === PROTOCOL_FIELD
    );
    let commandNumberField = Object.keys(fieldMapping()).find(
      (field) => fieldMapping()[field] === COMMAND_NUMBER_FIELD
    );
    let deviceField = Object.keys(fieldMapping()).find(
      (field) => fieldMapping()[field] === DEVICE_FIELD
    );
    let commandTypeField = Object.keys(fieldMapping()).find(
      (field) => fieldMapping()[field] === COMMAND_TYPE_FIELD
    );
    let titleField = Object.keys(fieldMapping()).find(
      (field) => fieldMapping()[field] === TITLE_FIELD
    );
    let commands = csvArray()
      .map((row) => {
        let protocol = valueMapping()[PROTOCOL_FIELD][row[protocolField]];
        if (!protocol) return null;
        let commandNumber = row[commandNumberField];
        if (isNaN(commandNumber)) return null;
        commandNumber = parseInt(commandNumber);
        let device = row[deviceField];
        if (isNaN(device)) return null;
        device = parseInt(device);
        let commandType =
          valueMapping()[COMMAND_TYPE_FIELD][row[commandTypeField]];
        if (!commandType) return null;
        let title = row[titleField];
        let command = new Command({
          protocol,
          commandNumber,
          device,
          commandType,
          title,
        });
        return command;
      })
      .filter((command) => command !== null);
    setCommands(commands);
    nextStep();
  }

  function ConfirmStep() {
    return (
      <div class="mb-3">
        <div>Confirm:</div>
        <div class="d-flex flex-column">
          {commands().map((command) => (
            <div class="d-flex flex-row align-items-center justify-content-center mb-2">
              <div class="col-sm-2">{command.getProtocol()}</div>
              <div class="col-sm-2">{command.getCommandNumber()}</div>
              <div class="col-sm-2">{command.getDevice()}</div>
              <div class="col-sm-2">{command.getCommandType()}</div>
              <div class="col-sm-2">{command.getTitle()}</div>
            </div>
          ))}
        </div>
      </div>
    );
  }

  function nextStep() {
    if (currentStep() >= TOTAL_STEPS) return;
    setError("");
    setCurrentStep(currentStep() + 1);
  }

  function previousStep() {
    if (currentStep() <= 1) return;
    setError("");
    setCurrentStep(currentStep() - 1);
  }

  function resetFields() {
    setCsvString("");
    setError("");
  }

  return (
    <Modal
      ref={props.ref}
      id="importCommandsModal"
      modalTitle="Import Commands from CSV"
      centered={true}
    >
      <div class="modal-body">
        <Show when={error() !== ""}>
          <div class="alert alert-danger" role="alert">
            {error()}
          </div>
        </Show>
        <Switch>
          <Match when={currentStep() === ENTER_CSV_STEP}>
            <EnterCsvStep />
          </Match>
          <Match when={currentStep() === MAP_FIELDS_STEP}>
            <MapFieldsStep />
          </Match>
          <Match when={currentStep() === MAP_VALUES_STEP}>
            <MapValuesStep />
          </Match>
          <Match when={currentStep() === CONFIRM_STEP}>
            <ConfirmStep />
          </Match>
        </Switch>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
          Cancel
        </button>
        <Show when={currentStep() > 1}>
          <button
            type="button"
            onClick={previousStep}
            class="btn btn-secondary"
            disabled={false}
          >
            Back
          </button>
        </Show>
        <Show when={currentStep() < TOTAL_STEPS}>
          <button
            type="button"
            onClick={handleNextStep}
            class="btn btn-secondary"
            disabled={!canMakeNextStep()}
          >
            Next
          </button>
        </Show>
        <Show when={currentStep() === TOTAL_STEPS}>
          <button
            type="button"
            onClick={handleImportCommands}
            class="btn btn-primary"
            disabled={false}
          >
            Import
          </button>
        </Show>
      </div>
    </Modal>
  );
}

ImportCommandsModal.Handler = new ModalHandler();
ImportCommandsModal.onCommandsImported = (callback) =>
  eventEmitter.on(COMMANDS_IMPORTED_EVENT, callback);

export default ImportCommandsModal;
