import * as PC from 'polygon-clipping';

import * as Util from '@dra2020/util';
import * as FSM from '@dra2020/fsm';

import * as Poly from './poly';
import * as Q from './quad';

// Confusion on how polygon-clipping exposes its interface - be flexible
let _union: any = undefined;
let _difference: any = undefined;
let anyPC: any = PC;
if (anyPC.union) _union = anyPC.union;
else if (anyPC.default.union) _union = anyPC.default.union;
if (_union === undefined) throw 'Unable to load union function from polygon-clipping';
if (anyPC.difference) _difference = anyPC.difference;
else if (anyPC.default.difference) _difference = anyPC.default.difference;
if (_difference === undefined) throw 'Unable to load difference function from polygon-clipping';

const FSM_COMPUTING = FSM.FSM_CUSTOM1;

class FsmDifference extends FSM.Fsm
{
  accum: any;
  polys: any[];

  constructor(env: FSM.FsmEnvironment, accum?: any, polys?: any[])
  {
    super(env);
    this.initialize(accum, polys);
  }

  initialize(accum: any, polys: any[])
  {
    this.accum = accum;
    this.polys = polys;
    if (polys == null || polys.length == 0)
      this.setState(FSM.FSM_DONE);
    else
      this.setState(FSM.FSM_STARTING);
  }

  cancel(): void
  {
    this.accum = null;
    this.polys = null;
    this.setState(FSM.FSM_DONE);
  }

  get result(): any
  {
    return this.accum;
  }

  tick(): void
  {
    if (this.ready)
    {
      switch (this.state)
      {
        case FSM.FSM_STARTING:
          if (this.polys == null)
            this.setState(FSM.FSM_DONE);
          else
            this.setState(FSM_COMPUTING);
          break;

        case FSM_COMPUTING:
          this.accum = _difference(this.accum, this.polys);
          this.polys = null;
          this.setState(FSM.FSM_DONE);
          break;
      }
    }
  }
}

const FSM_UNION = FSM.FSM_CUSTOM1;
const FSM_DIFFERENCE = FSM.FSM_CUSTOM2;

class FsmIncrementalUnion extends FSM.Fsm
{
  options: Poly.TickOptions;
  key: any;
  map: any; // { [geoid: string]: Feature }
  result: any;
  lastCompleteMap: any;
  lastCompleteResult: any;
  toSub: any[];
  fsmUnion: Q.FsmQuadTree;
  fsmDifference: FsmDifference;

  constructor(env: FSM.FsmEnvironment, options: Poly.TickOptions, key: any, map?: any)
  {
    super(env);
    this.options = options;
    this.key = key;
    this.result = null;
    this.map = null;
    this.lastCompleteResult = null;
    this.lastCompleteMap = null;
    this.toSub = null;
    this.fsmUnion = null;
    this.fsmDifference = null;
    if (map) this.recompute(map);
  }

  matches(key: any): boolean
  {
    return Util.shallowEqual(this.key, key);
  }

  recompute(map: any): void
  {
    // If a computation is in progress, just cancel and restart
    if (this.fsmUnion)
    {
      this.fsmUnion.cancel();
      this.fsmUnion = null;
      this.result = this.lastCompleteResult;
      this.map = this.lastCompleteMap;
    }
    if (this.fsmDifference)
    {
      this.fsmDifference.cancel();
      this.fsmDifference = null;
      this.result = this.lastCompleteResult;
      this.map = this.lastCompleteMap;
    }

    let polys: any[] = [];
    if (this.result == null || Util.isEmpty(map))
    {
      // Starting from scratch
      this.toSub = null;
      for (let id in map) if (map.hasOwnProperty(id))
        polys.push(map[id].geometry.coordinates);
    }
    else
    {
      // We have the result of a previous computation - compute a difference since we assume
      // that is cheaper (often only one or two polygons added or removed).
      let id: string;
      let nSkipped: number = 0;

      // To add
      polys.push(this.result);
      this.result = null;
      for (id in map) if (map.hasOwnProperty(id))
        if (this.map[id] === undefined)
          polys.push(map[id].geometry.coordinates);
        else
          nSkipped++;

      this.toSub = [];
      if (nSkipped > 0)
      {
        // To sub
        for (id in this.map) if (this.map.hasOwnProperty(id) && map[id] === undefined)
          this.toSub.push(this.map[id].geometry.coordinates);
      }
      if (this.toSub.length == 0)
        this.toSub = null;
    }
    this.fsmUnion = new Q.FsmQuadTree(this.env, this.options, polys);
    this.waitOn(this.fsmUnion);
    this.setState(FSM_UNION);
    this.map = map;
  }

  cancel(): void
  {
    if (this.fsmUnion)
    {
      this.fsmUnion.cancel();
      this.fsmUnion = null;
    }
    if (this.fsmDifference)
    {
      this.fsmDifference.cancel();
      this.fsmDifference = null;
    }
    this.result = null;
    this.map = null;
    this.lastCompleteResult = null;
    this.lastCompleteMap = null;
    this.setState(FSM.FSM_DONE);
  }

  tick(): void
  {
    if (this.ready)
    {
      switch (this.state)
      {
        case FSM.FSM_STARTING:
          // never initialized to do work (see recompute())
          this.setState(FSM.FSM_DONE);
          break;

        case FSM_UNION:
          this.fsmDifference = new FsmDifference(this.env, this.fsmUnion.result, this.toSub);
          this.waitOn(this.fsmDifference);
          this.toSub = null;
          this.fsmUnion = null;
          this.setState(FSM_DIFFERENCE);
          break;

        case FSM_DIFFERENCE:
          this.result = this.fsmDifference.result;
          this.lastCompleteResult = this.result;
          this.lastCompleteMap = this.map;
          this.fsmDifference = null;
          this.setState(FSM.FSM_DONE);
          break;
      }
    }
  }
}

export interface UnionResult
{
  key: any;
  poly: any;
}

export class FsmUnion extends FSM.Fsm
{
  options: Poly.TickOptions;
  unions: FsmIncrementalUnion[];

  constructor(env: FSM.FsmEnvironment, options?: Poly.TickOptions)
  {
    super(env);
    this.options = Util.shallowAssignImmutable(Poly.DefaultTickOptions, options);
    this.unions = [];
  }

  get result(): UnionResult[]
  {
    if (this.unions.length > 0 && this.state === FSM.FSM_DONE)
      return this.unions.map((i: FsmIncrementalUnion) => ({ key: i.key, poly: i.result }) );
    else
      return null;
  }

  cancel(): void
  {
    this.unions.forEach((i: FsmIncrementalUnion) => {
        i.cancel();
      });
    this.unions = [];
    this.setState(FSM.FSM_DONE);
  }

  recompute(key: any, map: any): void
  {
    let fsm: FsmIncrementalUnion = this.unions.find((i: FsmIncrementalUnion) => i.matches(key));
    if (fsm == null)
    {
      fsm = new FsmIncrementalUnion(this.env, this.options, key, map);
      this.unions.push(fsm);
    }
    else
      fsm.recompute(map);
    this.waitOn(fsm);
    this.setState(FSM_COMPUTING);
  }

  tick(): void
  {
    if (this.ready)
    {
      switch (this.state)
      {
        case FSM.FSM_STARTING:
        case FSM_COMPUTING:
          this.setState(FSM.FSM_DONE);
          break;
      }
    }
  }
}
