import {
  AfterViewInit,
  Component,
  ElementRef,
  EventEmitter,
  Input,
  OnChanges,
  OnInit,
  Output,
  SimpleChanges,
  ViewChild,
} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../../../environments/environment';
import {algorithmNames, AnalysisService} from '../../services/analysis/analysis.service';
import {
  Drug,
  EdgeType,
  NodeAttributeMap,
  getDrugNodeId,
  getProteinNodeId,
  getWrapperFromNode,
  LegendContext,
  Node,
  Task,
  Tissue,
  Wrapper,
  NodeInteraction,
} from '../../interfaces';
import domtoimage from 'dom-to-image';
import {NetworkSettings} from '../../network-settings';
import {NetexControllerService} from 'src/app/services/netex-controller/netex-controller.service';
import {defaultConfig, IConfig} from 'src/app/config';
import {mapCustomEdge, mapCustomNode} from 'src/app/main-network';
import {downLoadFile, pieChartContextRenderer, removeDuplicateObjectsFromList} from 'src/app/utils';
import {DrugstoneConfigService} from 'src/app/services/drugstone-config/drugstone-config.service';
import {NetworkHandlerService} from 'src/app/services/network-handler/network-handler.service';
import {LegendService} from 'src/app/services/legend-service/legend-service.service';
import { LoadingScreenService } from 'src/app/services/loading-screen/loading-screen.service';

declare var vis: any;

interface Scored {
  score: number;  // Normalized or unnormalized (whichever user selects, will be displayed in the table)
  rawScore: number;  // Unnormalized (kept to restore unnormalized value)
}

interface Seeded {
  isSeed: boolean;
}


@Component({
  selector: 'app-analysis-panel',
  templateUrl: './analysis-panel.component.html',
  styleUrls: ['./analysis-panel.component.scss'],
})
export class AnalysisPanelComponent implements OnInit, OnChanges, AfterViewInit {

  @ViewChild('networkWithLegend', {static: false}) networkWithLegendEl: ElementRef;
  @Input() token: string | null = null;


  @Output() tokenChange = new EventEmitter<string | null>();
  @Output() showDetailsChange = new EventEmitter<Wrapper>();
  @Output() setInputNetwork = new EventEmitter<any>();
  @Output() visibleItems = new EventEmitter<[any[], [Node[], Tissue], NodeInteraction[]]>();
  public task: Task | null = null;
  public result: any = null;

  public fullscreen = false;

  public network: any;
  public nodeData: { nodes: any, edges: any } = {nodes: null, edges: null};
  // private drugNodes: any[] = [];
  // private drugEdges: any[] = [];
  public tab: 'meta' | 'network' | 'table' = 'table';

  // public adjacentDrugs = false;
  // public adjacentDrugList: Node[] = [];
  // public adjacentDrugEdgesList: Node[] = [];
  //
  // public adjacentDisordersProtein = false;
  // public adjacentDisordersDrug = false;
  //
  // public adjacentProteinDisorderList: Node[] = [];
  // public adjacentProteinDisorderEdgesList: Node[] = [];
  //
  // public adjacentDrugDisorderList: Node[] = [];
  // public adjacentDrugDisorderEdgesList: Node[] = [];

  private proteins: any;
  public effects: any;

  public tableDrugs: Array<Drug & Scored> = [];
  public tableProteins: Array<Node & Scored & Seeded> = [];
  public tableSelectedProteins: Array<Node & Scored & Seeded> = [];
  public tableNormalize = false;
  public tableHasScores = false;

  public LegendContext: LegendContext = 'drugTarget';

  public expressionExpanded = false;
  public selectedTissue: Tissue | null = null;

  public algorithmNames = algorithmNames;

  public tableDrugScoreTooltip = '';
  public tableProteinScoreTooltip = '';

  public expressionMap: NodeAttributeMap;

  public loading = false;

  constructor(public legendService: LegendService, public networkHandler: NetworkHandlerService, public drugstoneConfig: DrugstoneConfigService, private http: HttpClient, public analysis: AnalysisService, public netex: NetexControllerService, public loadingScreen: LoadingScreenService) {
  }

  async ngOnInit() {
  }

  ngAfterViewInit() {
    this.networkHandler.setActiveNetwork('analysis');
  }

  async ngOnChanges(changes: SimpleChanges) {
    await this.refresh();
  }

  private async refresh() {
    if (this.token) {
      this.loadingScreen.stateUpdate(true);
      this.task = await this.getTask(this.token);
      this.analysis.switchSelection(this.token);

      if (this.task.info.algorithm === 'degree') {
        this.tableDrugScoreTooltip =
          'Normalized number of direct interactions of the drug with the seeds. ' +
          'The higher the score, the more relevant the drug.';
        this.tableProteinScoreTooltip =
          'Normalized number of direct interactions of the protein with the seeds. ' +
          'The higher the score, the more relevant the protein.';
      } else if (this.task.info.algorithm === 'closeness' || this.task.info.algorithm === 'quick' || this.task.info.algorithm === 'super') {
        this.tableDrugScoreTooltip =
          'Normalized inverse mean distance of the drug to the seeds. ' +
          'The higher the score, the more relevant the drug.';
        this.tableProteinScoreTooltip =
          'Normalized inverse mean distance of the protein to the seeds. ' +
          'The higher the score, the more relevant the protein.';
      } else if (this.task.info.algorithm === 'trustrank') {
        this.tableDrugScoreTooltip =
          'Amount of ‘trust’ on the drug at termination of the algorithm. ' +
          'The higher the score, the more relevant the drug.';
        this.tableProteinScoreTooltip =
          'Amount of ‘trust’ on the protein at termination of the algorithm. ' +
          'The higher the score, the more relevant the protein.';
      } else if (this.task.info.algorithm === 'proximity') {
        this.tableDrugScoreTooltip =
          'Empirical z-score of mean minimum distance between the drug’s targets and the seeds. ' +
          'The lower the score, the more relevant the drug.';
        this.tableProteinScoreTooltip =
          'Empirical z-score of mean minimum distance between the drug’s targets and the seeds. ' +
          'The lower the score, the more relevant the drug.';
      }

      if (this.task && this.task.info.done) {
        this.loading = true;
        this.netex.getTaskResult(this.token).then(result => {
          this.drugstoneConfig.set_analysisConfig(result.parameters.config);
          this.result = result;
          if (this.result.parameters.target === 'drug') {
            this.legendService.add_to_context('drug');
          } else {
            this.legendService.add_to_context('drugTarget');
          }
          const nodeAttributes = this.result.nodeAttributes || {};

          this.networkHandler.activeNetwork.seedMap = nodeAttributes.isSeed || {};

          // Reset
          this.nodeData = {nodes: null, edges: null};
          this.networkHandler.activeNetwork.networkEl.nativeElement.innerHTML = '';
          this.networkHandler.activeNetwork.networkInternal = null;
          // Create
          this.createNetwork(this.result).then(nw => {
            const nodes = nw.nodes;
            const edges = nw.edges;

            this.networkHandler.activeNetwork.inputNetwork = {nodes: nodes, edges: edges};
            this.nodeData.nodes = new vis.DataSet(nodes);
            this.nodeData.edges = new vis.DataSet(edges);
            const container = this.networkHandler.activeNetwork.networkEl.nativeElement;
            const isBig = nodes.length > 100 || edges.length > 100;
            const options = NetworkSettings.getOptions(isBig ? 'analysis-big' : 'analysis', this.drugstoneConfig.currentConfig());
            // @ts-ignore
            options.groups = this.drugstoneConfig.currentConfig().nodeGroups;
            // @ts-ignore
            for (const g of Object.values(options.groups)) {
              // @ts-ignore
              delete g.renderer;
            }
            this.drugstoneConfig.config.physicsOn = !isBig;
            this.networkHandler.activeNetwork.networkInternal = new vis.Network(container, this.nodeData, options);
            this.networkHandler.activeNetwork.networkInternal.on('stabilizationIterationsDone', () => {
              if (!this.drugstoneConfig.config.physicsOn) {
                this.networkHandler.activeNetwork.updatePhysicsEnabled(false);
              }
              this.networkHandler.updateAdjacentNodes();
            });
            this.tableDrugs = nodes.filter(e => e.drugstoneId && e.drugstoneType === 'drug');
            this.tableDrugs.forEach((r) => {
              r.rawScore = r.score;
            });
            this.tableProteins = nodes.filter(e => e.drugstoneId && e.drugstoneType === 'protein');
            this.tableSelectedProteins = [];
            this.tableProteins.forEach((r) => {
              r.rawScore = r.score;
              r.isSeed = this.networkHandler.activeNetwork.seedMap[r.id];
              const wrapper = getWrapperFromNode(r);
              if (this.analysis.inSelection(wrapper)) {
                this.tableSelectedProteins.push(r);
              }
            });


            this.tableHasScores = ['trustrank', 'closeness', 'degree', 'betweenness', 'quick', 'super']
              .indexOf(this.task.info.algorithm) !== -1;
            if (this.tableHasScores) {
              this.toggleNormalization(true);
            }
            this.networkHandler.activeNetwork.networkInternal.setData({nodes: undefined, edge: undefined});
            setTimeout(() => {
              this.networkHandler.activeNetwork.networkInternal.setData(this.nodeData);
            }, 1000);

            this.networkHandler.activeNetwork.networkInternal.on('deselectNode', (properties) => {
              this.showDetailsChange.emit(null);
            });

            this.networkHandler.activeNetwork.networkInternal.on('doubleClick', (properties) => {
              const nodeIds: Array<string> = properties.nodes;
              if (nodeIds.length > 0) {
                const nodeId = nodeIds[0];
                const node = this.nodeData.nodes.get(nodeId);
                if (node.drugstoneId === undefined || node.nodeType === 'drug' || node.drugstoneType !== 'protein') {
                  this.analysis.unmappedNodeToast();
                  return;
                }
                const wrapper = getWrapperFromNode(node);
                if (this.analysis.inSelection(wrapper)) {
                  this.analysis.removeItems([wrapper]);
                  this.analysis.getCount();
                } else {
                  this.analysis.addItems([wrapper]);
                  this.analysis.getCount();
                }
              }
            });

            this.networkHandler.activeNetwork.networkInternal.on('click', (properties) => {
              const selectedNodes = this.nodeData.nodes.get(properties.nodes);
              if (selectedNodes.length > 0) {
                this.showDetailsChange.emit(getWrapperFromNode(selectedNodes[0]));
              } else {
                this.showDetailsChange.emit(null);
              }
            });

            this.analysis.subscribeList((items, selected) => {
              // return if analysis panel is closed or no nodes are loaded
              if (!this.token) {
                return;
              }

              if (selected !== null) {
                const updatedNodes: Node[] = [];
                for (const item of items) {
                  const node = this.nodeData.nodes.get(item.id);
                  if (!node) {
                    continue;
                  }
                  const pos = this.networkHandler.activeNetwork.networkInternal.getPositions([item.id]);
                  node.x = pos[item.id].x;
                  node.y = pos[item.id].y;
                  const isSeed = this.networkHandler.activeNetwork.highlightSeeds ? this.networkHandler.activeNetwork.seedMap[node.id] : false;
                  const nodeStyled = NetworkSettings.getNodeStyle(
                    node,
                    this.drugstoneConfig.currentConfig(),
                    isSeed,
                    selected,
                    this.networkHandler.activeNetwork.getGradient(item.id),
                    this.networkHandler.activeNetwork.nodeRenderer
                  );
                  updatedNodes.push(nodeStyled);
                }
                this.nodeData.nodes.update(updatedNodes);

                const proteinSelection = this.tableSelectedProteins;
                for (const item of items) {
                  // TODO: Refactor!
                  const found = proteinSelection.findIndex((i) => getProteinNodeId(i) === item.id);
                  const tableItem = this.tableProteins.find((i) => getProteinNodeId(i) === item.id);
                  if (selected && found === -1 && tableItem) {
                    proteinSelection.push(tableItem);
                  }
                  if (!selected && found !== -1 && tableItem) {
                    proteinSelection.splice(found, 1);
                  }
                }
                this.tableSelectedProteins = [...proteinSelection];
              } else {
                // else: selected is null
                const updatedNodes = [];
                this.nodeData.nodes.forEach((node) => {
                  const isSeed = this.networkHandler.activeNetwork.highlightSeeds ? this.networkHandler.activeNetwork.seedMap[node.id] : false;
                  if (!isSeed) {
                    return;
                  }
                  const nodeStyled = NetworkSettings.getNodeStyle(
                    node,
                    this.drugstoneConfig.currentConfig(),
                    isSeed,
                    selected,
                    this.networkHandler.activeNetwork.getGradient(node.id),
                    this.networkHandler.activeNetwork.nodeRenderer
                  );
                  updatedNodes.push(nodeStyled);
                });
                this.nodeData.nodes.update(updatedNodes);

                const proteinSelection = [];
                for (const item of items) {
                  const tableItem = this.tableProteins.find((i) => getProteinNodeId(i) === item.id);
                  if (tableItem) {
                    proteinSelection.push(tableItem);
                  }
                }
                this.tableSelectedProteins = [...proteinSelection];
              }
            });
            this.emitVisibleItems(true);
          });
        this.loadingScreen.stateUpdate(false);

        });
      }
    }


  }

  public emitVisibleItems(on: boolean) {
    if (on) {
      this.visibleItems.emit([this.nodeData.nodes, [this.proteins, this.selectedTissue], this.nodeData.edges]);
    } else {
      this.visibleItems.emit(null);
    }
  }

  private async getTask(token: string): Promise<any> {
    return await this.http.get(`${this.netex.getBackend()}task/?token=${token}`).toPromise();
  }

  close() {
    this.networkHandler.activeNetwork.gradientMap = {};
    this.drugstoneConfig.remove_analysisConfig();
    this.expressionExpanded = false;
    this.expressionMap = undefined;
    this.networkHandler.activeNetwork.seedMap = {};
    this.networkHandler.activeNetwork.highlightSeeds = false;
    this.analysis.switchSelection('main');
    this.token = null;
    this.tokenChange.emit(this.token);
    this.legendService.remove_from_context('drug');
    this.legendService.remove_from_context('drugTarget');
    this.emitVisibleItems(false);
  }

  public toggleNormalization(normalize: boolean) {
    this.tableNormalize = normalize;

    const normalizeFn = (table) => {
      let max = 0;
      table.forEach(i => {
        if (i.rawScore > max) {
          max = i.rawScore;
        }
      });
      table.forEach(i => {
        i.score = i.rawScore / max;
      });
    };

    const unnormalizeFn = (table) => {
      table.forEach(i => {
        i.score = i.rawScore;
      });
    };

    if (normalize) {
      normalizeFn(this.tableProteins);
      if (this.task.info.target === 'drug') {
        normalizeFn(this.tableDrugs);
      }
    } else {
      unnormalizeFn(this.tableProteins);
      if (this.task.info.target === 'drug') {
        unnormalizeFn(this.tableDrugs);
      }
    }
  }

  public downloadLink(view: string): string {
    return `${this.netex.getBackend()}task_result/?token=${this.token}&view=${view}&fmt=csv`;
  }

  /**
   * Maps analysis result returned from database to valid Vis.js network input
   *
   * @param result
   * @returns
   */
  public async createNetwork(result: any): Promise<{ edges: any[]; nodes: any[]; }> {
    const identifier = this.drugstoneConfig.currentConfig().identifier;

    // add drugGroup and foundNodesGroup for added nodes
    // these groups can be overwritten by the user
    const nodes = [];
    let edges = [];

    const attributes = result.nodeAttributes || {};

    this.proteins = [];
    this.effects = [];
    const network = result.network;
    network.nodes = [...new Set<string>(network.nodes)];

    const details = attributes.details || {};
    const nodeIdMap = {};
    // @ts-ignore
    Object.entries(details).filter(e => e[1].drugstoneType === 'protein').forEach(e => {
      // @ts-ignore
      e[1].drugstoneId.forEach(id => {
        nodeIdMap[id] = e[1][identifier][0];
      });
    });
    for (const nodeId of network.nodes) {
      if (details[nodeId]) {
        const nodeDetails = details[nodeId];
        nodeDetails.id = nodeDetails.id ? nodeDetails.id : (typeof nodeDetails.drugstoneId === 'string' ? nodeDetails.drugstoneId : nodeDetails.drugstoneId[0]);
        if (nodeDetails.drugstoneId && nodeDetails.drugstoneType === 'protein') {
          // node is protein from database, has been mapped on init to backend protein from backend
          // or was found during analysis
          // FIXME connectorNodes are not visualized correctly
          nodeDetails.group = result.targetNodes && result.targetNodes.indexOf(nodeId) !== -1 ? 'foundNode' : (nodeDetails.group ? nodeDetails.group : 'connectorNode');
          nodeDetails.label = nodeDetails.label ? nodeDetails.label : nodeDetails[identifier];
          nodeDetails.id = nodeDetails[identifier][0] ? nodeDetails[identifier][0] : nodeDetails.id;
          this.proteins.push(nodeDetails);
        } else if (nodeDetails.drugstoneId && nodeDetails.drugstoneType === 'drug') {
          // node is drug, was found during analysis
          nodeDetails.type = 'Drug';
          nodeDetails.group = 'foundDrug';
        } else {
          // node is custom input from user, could not be mapped to backend protein
          nodeDetails.group = nodeDetails.group ? nodeDetails.group : 'default';
          nodeDetails.label = nodeDetails.label ? nodeDetails.label : nodeDetails[identifier];
        }
        // further analysis and the button function can be used to highlight seeds
        // option to use scores[node] as gradient, but sccores are very small
        nodes.push(NetworkSettings.getNodeStyle(nodeDetails as Node, this.drugstoneConfig.currentConfig(), false, false, 1, this.networkHandler.activeNetwork.nodeRenderer));
      } else {
        console.log('Missing details for ' + nodeId);
      }
    }
    const uniqEdges = [];
    for (const edge of network.edges) {
      const e = mapCustomEdge(edge, this.drugstoneConfig.currentConfig());
      e.from = e.from[0] === 'p' && nodeIdMap[e.from] ? nodeIdMap[e.from] : e.from;
      e.to = e.to[0] === 'p' && nodeIdMap[e.to] ? nodeIdMap[e.to] : e.to;
      const hash = e.from + '_' + e.to;
      if (uniqEdges.indexOf(hash) === -1) {
        uniqEdges.push(hash);
        edges.push(e);
      }
    }
    // remove self-edges/loops
    if (!this.drugstoneConfig.currentConfig().selfReferences) {
      edges = edges.filter(el => el.from !== el.to);
    }
    return {
      nodes,
      edges,
    };
  }

  getResultNodes() {
    if (this.nodeData && this.nodeData['nodes']) {
      return this.nodeData['nodes'].get();
    }
    return [];
  }

  getResultEdges() {
    if (this.nodeData && this.nodeData['edges']) {
      return this.nodeData['edges'].get().filter(e => !e.id || !e.groupName || (typeof e.from === 'string' && typeof e.to === 'string'));
    }
    return [];
  }

  public tableProteinSelection = (e): void => {
    const oldSelection = [...this.tableSelectedProteins];
    this.tableSelectedProteins = e;
    const addItems = [];
    const removeItems = [];
    for (const i of this.tableSelectedProteins) {
      const wrapper = getWrapperFromNode(i);
      if (oldSelection.indexOf(i) === -1) {
        addItems.push(wrapper);
      }
    }
    for (const i of oldSelection) {
      const wrapper = getWrapperFromNode(i);
      if (this.tableSelectedProteins.indexOf(i) === -1) {
        removeItems.push(wrapper);
      }
    }
    this.analysis.addItems(addItems);
    this.analysis.removeItems(removeItems);
  };

  public toggleFullscreen() {
    this.fullscreen = !this.fullscreen;
    this.loadingScreen.fullscreenUpdate(this.fullscreen)
  }
}