// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import { PhaseView } from "../src/view";
import { anyToString, ViewElements, isIterable } from "../src/util";
import { MySelection } from "../src/selection";
import { SourceResolver } from "./source-resolver";
import { SelectionBroker } from "./selection-broker";
import { NodeSelectionHandler, BlockSelectionHandler, RegisterAllocationSelectionHandler } from "./selection-handler";

export abstract class TextView extends PhaseView {
  selectionHandler: NodeSelectionHandler;
  blockSelectionHandler: BlockSelectionHandler;
  registerAllocationSelectionHandler: RegisterAllocationSelectionHandler;
  selection: MySelection;
  blockSelection: MySelection;
  registerAllocationSelection: MySelection;
  textListNode: HTMLUListElement;
  instructionIdToHtmlElementsMap: Map<string, Array<HTMLElement>>;
  nodeIdToHtmlElementsMap: Map<string, Array<HTMLElement>>;
  blockIdToHtmlElementsMap: Map<string, Array<HTMLElement>>;
  blockIdtoNodeIds: Map<string, Array<string>>;
  nodeIdToBlockId: Array<string>;
  patterns: any;
  sourceResolver: SourceResolver;
  broker: SelectionBroker;

  constructor(id, broker) {
    super(id);
    const view = this;
    view.textListNode = view.divNode.getElementsByTagName('ul')[0];
    view.patterns = null;
    view.instructionIdToHtmlElementsMap = new Map();
    view.nodeIdToHtmlElementsMap = new Map();
    view.blockIdToHtmlElementsMap = new Map();
    view.blockIdtoNodeIds = new Map();
    view.nodeIdToBlockId = [];
    view.selection = new MySelection(anyToString);
    view.blockSelection = new MySelection(anyToString);
    view.broker = broker;
    view.sourceResolver = broker.sourceResolver;
    const selectionHandler = {
      clear: function () {
        view.selection.clear();
        view.updateSelection();
        broker.broadcastClear(selectionHandler);
      },
      select: function (nodeIds, selected) {
        view.selection.select(nodeIds, selected);
        view.updateSelection();
        broker.broadcastNodeSelect(selectionHandler, view.selection.selectedKeys(), selected);
      },
      brokeredNodeSelect: function (nodeIds, selected) {
        const firstSelect = view.blockSelection.isEmpty();
        view.selection.select(nodeIds, selected);
        view.updateSelection(firstSelect);
      },
      brokeredClear: function () {
        view.selection.clear();
        view.updateSelection();
      }
    };
    this.selectionHandler = selectionHandler;
    broker.addNodeHandler(selectionHandler);
    view.divNode.addEventListener('click', e => {
      if (!e.shiftKey) {
        view.selectionHandler.clear();
      }
      e.stopPropagation();
    });

    const blockSelectionHandler = {
      clear: function () {
        view.blockSelection.clear();
        view.updateSelection();
        broker.broadcastClear(blockSelectionHandler);
      },
      select: function (blockIds, selected) {
        view.blockSelection.select(blockIds, selected);
        view.updateSelection();
        broker.broadcastBlockSelect(blockSelectionHandler, blockIds, selected);
      },
      brokeredBlockSelect: function (blockIds, selected) {
        const firstSelect = view.blockSelection.isEmpty();
        view.blockSelection.select(blockIds, selected);
        view.updateSelection(firstSelect);
      },
      brokeredClear: function () {
        view.blockSelection.clear();
        view.updateSelection();
      }
    };
    this.blockSelectionHandler = blockSelectionHandler;
    broker.addBlockHandler(blockSelectionHandler);

    view.registerAllocationSelection = new MySelection(anyToString);
    const registerAllocationSelectionHandler = {
      clear: function () {
        view.registerAllocationSelection.clear();
        view.updateSelection();
        broker.broadcastClear(registerAllocationSelectionHandler);
      },
      select: function (instructionIds, selected) {
        view.registerAllocationSelection.select(instructionIds, selected);
        view.updateSelection();
        broker.broadcastInstructionSelect(null, [instructionIds], selected);
      },
      brokeredRegisterAllocationSelect: function (instructionIds, selected) {
        const firstSelect = view.blockSelection.isEmpty();
        view.registerAllocationSelection.select(instructionIds, selected);
        view.updateSelection(firstSelect);
      },
      brokeredClear: function () {
        view.registerAllocationSelection.clear();
        view.updateSelection();
      }
    };
    broker.addRegisterAllocatorHandler(registerAllocationSelectionHandler);
    view.registerAllocationSelectionHandler = registerAllocationSelectionHandler;
  }

  // instruction-id are the divs for the register allocator phase
  addHtmlElementForInstructionId(anyInstructionId: any, htmlElement: HTMLElement) {
    const instructionId = anyToString(anyInstructionId);
    if (!this.instructionIdToHtmlElementsMap.has(instructionId)) {
      this.instructionIdToHtmlElementsMap.set(instructionId, []);
    }
    this.instructionIdToHtmlElementsMap.get(instructionId).push(htmlElement);
  }

  addHtmlElementForNodeId(anyNodeId: any, htmlElement: HTMLElement) {
    const nodeId = anyToString(anyNodeId);
    if (!this.nodeIdToHtmlElementsMap.has(nodeId)) {
      this.nodeIdToHtmlElementsMap.set(nodeId, []);
    }
    this.nodeIdToHtmlElementsMap.get(nodeId).push(htmlElement);
  }

  addHtmlElementForBlockId(anyBlockId, htmlElement) {
    const blockId = anyToString(anyBlockId);
    if (!this.blockIdToHtmlElementsMap.has(blockId)) {
      this.blockIdToHtmlElementsMap.set(blockId, []);
    }
    this.blockIdToHtmlElementsMap.get(blockId).push(htmlElement);
  }

  addNodeIdToBlockId(anyNodeId, anyBlockId) {
    const blockId = anyToString(anyBlockId);
    if (!this.blockIdtoNodeIds.has(blockId)) {
      this.blockIdtoNodeIds.set(blockId, []);
    }
    this.blockIdtoNodeIds.get(blockId).push(anyToString(anyNodeId));
    this.nodeIdToBlockId[anyNodeId] = blockId;
  }

  blockIdsForNodeIds(nodeIds) {
    const blockIds = [];
    for (const nodeId of nodeIds) {
      const blockId = this.nodeIdToBlockId[nodeId];
      if (blockId == undefined) continue;
      blockIds.push(blockId);
    }
    return blockIds;
  }

  updateSelection(scrollIntoView: boolean = false) {
    if (this.divNode.parentNode == null) return;
    const mkVisible = new ViewElements(this.divNode.parentNode as HTMLElement);
    const view = this;
    const elementsToSelect = view.divNode.querySelectorAll(`[data-pc-offset]`);
    for (const el of elementsToSelect) {
      el.classList.toggle("selected", false);
    }
    for (const [blockId, elements] of this.blockIdToHtmlElementsMap.entries()) {
      const isSelected = view.blockSelection.isSelected(blockId);
      for (const element of elements) {
        mkVisible.consider(element, isSelected);
        element.classList.toggle("selected", isSelected);
      }
    }

    for (const key of this.instructionIdToHtmlElementsMap.keys()) {
      for (const element of this.instructionIdToHtmlElementsMap.get(key)) {
        element.classList.toggle("selected", false);
      }
    }
    for (const instrId of view.registerAllocationSelection.selectedKeys()) {
      const elements = this.instructionIdToHtmlElementsMap.get(instrId);
      if (!elements) continue;
      for (const element of elements) {
        mkVisible.consider(element, true);
        element.classList.toggle("selected", true);
      }
    }

    for (const key of this.nodeIdToHtmlElementsMap.keys()) {
      for (const element of this.nodeIdToHtmlElementsMap.get(key)) {
        element.classList.toggle("selected", false);
      }
    }
    for (const nodeId of view.selection.selectedKeys()) {
      const elements = this.nodeIdToHtmlElementsMap.get(nodeId);
      if (!elements) continue;
      for (const element of elements) {
        mkVisible.consider(element, true);
        element.classList.toggle("selected", true);
      }
    }
    mkVisible.apply(scrollIntoView);
  }

  setPatterns(patterns) {
    this.patterns = patterns;
  }

  clearText() {
    while (this.textListNode.firstChild) {
      this.textListNode.removeChild(this.textListNode.firstChild);
    }
  }

  createFragment(text, style) {
    const fragment = document.createElement("SPAN");

    if (typeof style.associateData == 'function') {
      if (style.associateData(text, fragment) === false) {
         return null;
      }
    } else {
      if (style.css != undefined) {
        const css = isIterable(style.css) ? style.css : [style.css];
        for (const cls of css) {
          fragment.classList.add(cls);
        }
      }
      fragment.innerText = text;
    }

    return fragment;
  }

  processLine(line) {
    const view = this;
    const result = [];
    let patternSet = 0;
    while (true) {
      const beforeLine = line;
      for (const pattern of view.patterns[patternSet]) {
        const matches = line.match(pattern[0]);
        if (matches != null) {
          if (matches[0] != '') {
            const style = pattern[1] != null ? pattern[1] : {};
            const text = matches[0];
            if (text != '') {
              const fragment = view.createFragment(matches[0], style);
              if (fragment !== null) result.push(fragment);
            }
            line = line.substr(matches[0].length);
          }
          let nextPatternSet = patternSet;
          if (pattern.length > 2) {
            nextPatternSet = pattern[2];
          }
          if (line == "") {
            if (nextPatternSet != -1) {
              throw ("illegal parsing state in text-view in patternSet" + patternSet);
            }
            return result;
          }
          patternSet = nextPatternSet;
          break;
        }
      }
      if (beforeLine == line) {
        throw ("input not consumed in text-view in patternSet" + patternSet);
      }
    }
  }

  processText(text) {
    const view = this;
    const textLines = text.split(/[\n]/);
    let lineNo = 0;
    for (const line of textLines) {
      const li = document.createElement("LI");
      li.className = "nolinenums";
      li.dataset.lineNo = "" + lineNo++;
      const fragments = view.processLine(line);
      for (const fragment of fragments) {
        li.appendChild(fragment);
      }
      view.textListNode.appendChild(li);
    }
  }

  initializeContent(data, rememberedSelection) {
    this.clearText();
    this.processText(data);
    this.show();
  }

  public onresize(): void {}
}
