import { List } from 'immutable';
import { Class, Instance, isInstanceOf } from 'immutable-class';
import { Timezone, Duration, day, hour } from 'chronoshift';
import { $, Expression, ChainExpression, ExpressionJS, Action, ActionJS, SortAction, LimitAction, TimeBucketAction, NumberBucketAction } from 'plywood';
import { Dimension } from '../dimension/dimension';

export interface SplitCombineValue {
  expression: Expression;
  bucketAction: Action;
  sortAction: SortAction;
  limitAction: LimitAction;
}

export type SplitCombineJS = string | SplitCombineJSFull
export interface SplitCombineJSFull {
  expression: ExpressionJS;
  bucketAction?: ActionJS;
  sortAction?: ActionJS;
  limitAction?: ActionJS;
}

export interface SplitCombineContext {
  dimensions: List<Dimension>;
}

var check: Class<SplitCombineValue, SplitCombineJS>;
export class SplitCombine implements Instance<SplitCombineValue, SplitCombineJS> {
  static isSplitCombine(candidate: any): candidate is SplitCombine {
    return isInstanceOf(candidate, SplitCombine);
  }

  static fromExpression(expression: Expression): SplitCombine {
    return new SplitCombine({
      expression,
      bucketAction: null,
      sortAction: null,
      limitAction: null
    });
  }

  static fromJS(parameters: SplitCombineJS, context?: SplitCombineContext): SplitCombine {
    if (typeof parameters === 'string') {
      if (!context) throw new Error('must have context for string split');
      var dimension = context.dimensions.find(d => d.name === parameters);
      if (!dimension) throw new Error(`can not find dimension ${parameters}`);
      return new SplitCombine({
        expression: dimension.expression,
        bucketAction: null,
        sortAction: null,
        limitAction: null
      });
    } else {
      var value: SplitCombineValue = {
        expression: Expression.fromJSLoose(parameters.expression),
        bucketAction: null,
        sortAction: null,
        limitAction: null
      };

      if (parameters.bucketAction) value.bucketAction = Action.fromJS(parameters.bucketAction);
      if (parameters.sortAction) value.sortAction = SortAction.fromJS(parameters.sortAction);
      if (parameters.limitAction) value.limitAction = LimitAction.fromJS(parameters.limitAction);
      return new SplitCombine(value);
    }
  }


  public expression: Expression;
  public bucketAction: Action;
  public sortAction: SortAction;
  public limitAction: LimitAction;

  constructor(parameters: SplitCombineValue) {
    this.expression = parameters.expression;
    if (!this.expression) throw new Error('must have expression');
    this.bucketAction = parameters.bucketAction;
    this.sortAction = parameters.sortAction;
    this.limitAction = parameters.limitAction;
  }

  public valueOf(): SplitCombineValue {
    return {
      expression: this.expression,
      bucketAction: this.bucketAction,
      sortAction: this.sortAction,
      limitAction: this.limitAction
    };
  }

  public toJS(): SplitCombineJS {
    var js: SplitCombineJSFull = {
      expression: this.expression.toJS()
    };
    if (this.bucketAction) js.bucketAction = this.bucketAction.toJS();
    if (this.sortAction) js.sortAction = this.sortAction.toJS();
    if (this.limitAction) js.limitAction = this.limitAction.toJS();
    return js;
  }

  public toJSON(): SplitCombineJS {
    return this.toJS();
  }

  public toString(): string {
    return `[SplitCombine: ${this.expression}]`;
  }

  public equals(other: SplitCombine): boolean {
    var { expression, bucketAction, sortAction, limitAction } = this;
    return SplitCombine.isSplitCombine(other) &&
      expression.equals(other.expression) &&
      Boolean(bucketAction) === Boolean(other.bucketAction) &&
      (!bucketAction || bucketAction.equals(other.bucketAction)) &&
      Boolean(sortAction) === Boolean(other.sortAction) &&
      (!sortAction || sortAction.equals(other.sortAction)) &&
      Boolean(limitAction) === Boolean(other.limitAction) &&
      (!limitAction || limitAction.equals(other.limitAction));
  }

  public equalsByExpression(other: SplitCombine): boolean {
    var { expression } = this;
    return SplitCombine.isSplitCombine(other) && expression.equals(other.expression);
  }

  public toSplitExpression(): Expression {
    var { expression, bucketAction } = this;
    if (!bucketAction) return expression;
    return expression.performAction(bucketAction);
  }

  public toKey(): string {
    return this.toSplitExpression().toString();
  }

  public changeBucketAction(bucketAction: Action): SplitCombine {
    var value = this.valueOf();
    value.bucketAction = bucketAction;
    return new SplitCombine(value);
  }

  public changeSortAction(sortAction: SortAction): SplitCombine {
    var value = this.valueOf();
    value.sortAction = sortAction;
    return new SplitCombine(value);
  }

  public changeLimitAction(limitAction: LimitAction): SplitCombine {
    var value = this.valueOf();
    value.limitAction = limitAction;
    return new SplitCombine(value);
  }

  public changeLimit(limit: number): SplitCombine {
    var limitAction = limit === null ? null : new LimitAction({ limit });
    return this.changeLimitAction(limitAction);
  }

  public timezoneDependant(): boolean {
    const { bucketAction } = this;
    if (!bucketAction) return false;
    return bucketAction.needsEnvironment();
  }

  public getDimension(dimensions: List<Dimension>): Dimension {
    return Dimension.getDimensionByExpression(dimensions, this.expression);
  }

  public getTitle(dimensions: List<Dimension>): string {
    var dimension = this.getDimension(dimensions);
    return (dimension ? dimension.title : '?') + this.getBucketTitle();
  }

  public getBucketTitle(): string {
    var bucketAction = this.bucketAction;
    if (bucketAction instanceof TimeBucketAction) {
      return ` (${bucketAction.duration.getDescription(true)})`;
    } else if (bucketAction instanceof NumberBucketAction) {
      return ` (by ${bucketAction.size})`;
    }
    return '';
  }

}
