import * as _ from 'lodash';

import { camelCase, equalByType, extractor, filterListType, pascalCase, snakeCase } from './utils';
import { ClassNameModel } from './settings';
import { Input } from './input';
import { TypeDefinition } from './constructor';
import { emptyClass } from './syntax/empty-class.syntax';

export const emptyListWarn = 'list is empty';
export const ambiguousListWarn = 'list is ambiguous';
export const ambiguousTypeWarn = 'type is ambiguous';

export class Warning {
  warning: string;
  path: string;

  constructor(warning: string, path: string) {
    this.warning = warning;
    this.path = path;
  }
}

export function newEmptyListWarn(path: string): Warning {
  return new Warning(emptyListWarn, path);
}

export function newAmbiguousListWarn(path: string): Warning {
  return new Warning(ambiguousListWarn, path);
}

export function newAmbiguousType(path: string): Warning {
  return new Warning(ambiguousTypeWarn, path);
}

export class WithWarning<T> {
  result: T;
  warnings: Warning[];

  constructor(result: T, warnings: Warning[]) {
    this.result = result;
    this.warnings = warnings;
  }
}

/**
 * Prints a string to a line.
 * @param {string} print string that will be printed.
 * @param {number} lines how many lines will be added.
 * @param {number} tabs how many tabs will be added.
 */
export const printLine = (print: string, lines = 0, tabs = 0): string => {
  var sb = '';

  for (let i = 0; i < lines; i++) {
    sb += '\n';
  }
  for (let i = 0; i < tabs; i++) {
    sb += '\t';
  }

  sb += print;
  return sb;
};

const includeIfNull = (jsonValue: string, input: Input): string => {
  const include = input.includeIfNull === true && input.nullSafety === false;
  return include ? `${jsonValue} == null ? null : ` : '';
};

const jsonMapType = (input: Input): string => {
  const type = input.nullSafety && input.avoidDynamicTypes ? 'Object?' : 'dynamic';
  return `Map<String, ${type}>`;
};

/**
 *  Suffix for methods to/from.
 * @param input the user input.
 * @returns string
 */
const suffix = (input: Input) => {
  const _suffix = input.fromAndToSuffix;
  const suffix = input.jsonCodecs && _suffix.toLowerCase() === 'json' ? 'Map' : _suffix;
  return input.codeGenerator === 'JSON' ? 'Json' : suffix;
};

/**
 * Adds JSON annotation only if needed for Freezed and JSON serializable.
 * @param {string} jsonKey a raw JSON key.
 */
const jsonKeyAnnotation = (name: string, jsonKey: string): string => {
  return name !== jsonKey ? `@JsonKey(name: '${jsonKey}') ` : '';
};

/**
 * To indicate that a variable might have the value null.
 * @param {Input} input the user input.
 * @returns string as "?" if null safety enabled. Otherwise empty string.
 */
const questionMark = (input: Input, typeDef?: TypeDefinition): string => {
  if (typeDef) {
    return input.nullSafety && typeDef.nullable ? '?' : '';
  } else {
    return input.nullSafety ? '?' : '';
  }
};

/**
 * To indicate that a variable might have the value null.
 * @param {Input} input the user input.
 * @returns string as "?" if null safety enabled. Otherwise empty string.
 */
const defaultValue = (
  typeDef: TypeDefinition,
  nullable: boolean = false,
  freezed: boolean = false,
): string => {
  if (!typeDef.defaultValue) { return ''; }
  if (typeDef.isList && typeDef.type !== null) {
    const listType = typeDef.type?.replace(/List/g, '');
    const listTypes = filterListType(typeDef.type);
    if (!freezed) {
      const withType = ` = const ${listType}[]`;
      const withoutType = ' = const []';
      return listTypes.length > 1 ? withoutType : withType;
    } else {
      const withType = ` @Default(${listType}[])`;
      const withoutType = ' @Default([])';
      return listTypes.length > 1 ? withoutType : withType;
    }
  } else if (typeDef.isDate) {
    if (!freezed) {
      const questionMark = nullable ? '?' : '';
      return `${typeDef.type}${questionMark} ${typeDef.name}`;
    } else {
      return '';
    }
  } else if (typeDef.type?.startsWith('String')) {
    const freezedDefaultString = ` @Default('${typeDef.value}')`;
    const defaultString = ` = '${typeDef.value}'`;
    return freezed ? freezedDefaultString : defaultString;
  }
  const freezedDefaultValue = ` @Default(${typeDef.value})`;
  const defaultValue = ` = ${typeDef.value}`;
  return freezed ? freezedDefaultValue : defaultValue;
};

const defaultDateTime = (
  fields: Dependency[],
  input: Input,
): string => {
  let sb = '';
  const dates = fields.filter(
    (v) => v.typeDef.isDate && !v.typeDef.isList && v.typeDef.defaultValue
  );
  if (!dates.length) { return sb; }
  if (input.freezed) {
    for (let i = 0; i < dates.length; i++) {
      const typeDef = dates[i].typeDef;
      const optional = 'optional' + pascalCase(typeDef.name);
      const expressionBody = (): string => {
        let body = '';
        body += printLine(`DateTime get ${typeDef.name} => ${optional}`, 1, 1);
        body += printLine(` ?? ${parseDateTime(`'${typeDef.value}'`, true, typeDef, input)};`);
        return body;
      };
      const blockBody = (): string => {
        let body = '';
        body += printLine(`DateTime get ${typeDef.name} {`, 1, 1);
        body += printLine(`return ${optional} ?? ${parseDateTime(`'${typeDef.value}'`, true, typeDef, input)};`, 1, 2);
        body += printLine('}', 1, 1);
        return body;
      };
      sb += '\n';
      if (!input.nullSafety) {
        sb += printLine('@late', 1, 1);
        sb += expressionBody();
      } else {
        sb += expressionBody().length > 78 ? blockBody() : expressionBody();
      }
    }
  } else {
    for (let i = 0; i < dates.length; i++) {
      const typeDef = dates[i].typeDef;
      const comma: string = dates.length - 1 === i ? '' : ',';
      if (i === 0) {
        sb += printLine(`\t: ${typeDef.name} = ${typeDef.name}`);
        sb += printLine(` ?? ${parseDateTime(`'${typeDef.value}'`, true, typeDef, input)}`) + comma;
      } else {
        sb += printLine(`\n\t\t\t\t${typeDef.name} = ${typeDef.name}`);
        sb += printLine(` ?? ${parseDateTime(`'${typeDef.value}'`, true, typeDef, input)}`) + comma;
      }
    }
  }
  return sb;
};

const requiredValue = (required: boolean = false, nullSafety: boolean = false): string => {
  if (required) {
    return nullSafety ? 'required ' : '@required ';
  } else {
    return '';
  }
};

/**
 * Returns a string representation of a value obtained from a JSON
 * @param valueKey The key of the value in the JSON
 */
export const valueFromJson = (valueKey: string, input: Input): string => {
  const mapValue = input.jsonCodecs ? 'data' : 'json';
  return `${mapValue}['${valueKey}']`;
};

/**
 * Returns a string representation of a value beign assigned to a field/prop
 * @param key The field/prop name
 * @param value The value to assign to the field/prop
 */
export const joinAsClass = (key: string, value: string): string => {
  return `${key}: ${value},`;
};

const jsonParseClass = (key: string, typeDef: TypeDefinition, input: Input): string => {
  const jsonValue = valueFromJson(typeDef.jsonKey, input);
  const type = typeDef.type;
  // IMPORTANT. To keep the formatting correct.
  // By using block body. Default tabs are longTab = 5; shortTab = 3; 
  // By using expresion body. Default tabs are longTab = 6; shortTab = 4; 
  const longTab = 6;
  const shortTab = 4;
  let formatedValue = '';
  if (type !== null && !typeDef.isPrimitive || type !== null && typeDef.isDate) {
    if (typeDef.isList) {
      // Responsive farmatting.
      // List of List Classes (List<List.......<Class>>)
      // This will generate deeply nested infinity list depending on how many lists are in the lists.
      const result = filterListType(type);
      formatedValue = printLine(`(${jsonValue} as List<dynamic>${questionMark(input, typeDef)})`);
      for (let i = 0; i < result.length - 1; i++) {
        var index = i * 2;
        const tabs = longTab + index;
        if (input.nullSafety) {
          if (i === 0) {
            formatedValue += printLine('?.map((e) => (e as List<dynamic>)', 1, tabs);
          } else {
            formatedValue += printLine('.map((e) => (e as List<dynamic>)', 1, tabs);
          }
        } else {
          formatedValue += printLine('?.map((e) => (e as List<dynamic>)', 1, tabs);
        }
      }
      if (input.nullSafety) {
        const tabs = shortTab + 2 * result.length;
        if (result.length > 1) {
          if (typeDef.isDate) {
            formatedValue += printLine(`.map((e) => ${parseDateTime('e', false, typeDef, input)})`, 1, tabs);
          } else {
            formatedValue += printLine(`.map((e) => ${buildParseClass(key, 'e', typeDef, input)})`, 1, tabs);
          }
        } else {
          if (typeDef.isDate) {
            formatedValue += printLine(`?.map((e) => ${parseDateTime('e', false, typeDef, input)})`, 1, tabs);
          } else {
            formatedValue += printLine(`?.map((e) => ${buildParseClass(key, 'e', typeDef, input)})`, 1, tabs);
          }
        }
      } else {
        formatedValue += printLine('?.map((e) => e == null', 1, shortTab + 2 * result.length);
        formatedValue += printLine('? null', 1, longTab + 2 * result.length);
        if (typeDef.isDate) {
          formatedValue += printLine(`: ${parseDateTime('e', false, typeDef, input)})`, 1, longTab + 2 * result.length);
        } else {
          formatedValue += printLine(`: ${buildParseClass(key, 'e', typeDef, input)})`, 1, longTab + 2 * result.length);
        }
      }
      for (let i = 0; i < result.length - 1; i++) {
        var index = i * 2;
        const tabs = shortTab + 2 * result.length - index;
        formatedValue += printLine(input.nullSafety ? '.toList())' : '?.toList())', 1, tabs);
      }
      formatedValue += printLine(input.nullSafety ? '.toList()' : '?.toList()', 1, longTab);
    } else {
      // Class
      formatedValue += printLine(`${jsonValue} == null`);
      formatedValue += printLine('? null', 1, longTab);
      if (typeDef.isDate) {
        formatedValue += printLine(`: ${parseDateTime(jsonValue, false, typeDef, input)}`, 1, longTab);
      } else {
        formatedValue += printLine(`: ${buildParseClass(key, jsonValue, typeDef, input)}`, 1, longTab);
      }
    }
  }
  return formatedValue;
};

const toJsonClass = (
  typeDef: TypeDefinition,
  privateField: boolean,
  input: Input
): string => {
  const fieldKey = typeDef.getName(privateField);
  const thisKey = `${fieldKey}`;
  const type = typeDef.type;
  var sb = '';
  if (type !== null && !typeDef.isPrimitive || type !== null && typeDef.isDate) {
    if (typeDef.isList) {
      const result = filterListType(type);
      if (result.length > 1) {
        // Responsive formatting.
        // This will generate infiniti maps depending on how many lists are in the lists.
        // By default this line starts with keyword List, slice will remove it.
        if (input.nullSafety) {
          const isNullable = typeDef.nullable;
          sb += printLine(`'${typeDef.jsonKey}': ${thisKey}?`);
          sb += Array.from(result).map(_ => printLine('.map((e) => e')).slice(0, -1).join('');
          if (typeDef.isDate) {
            sb += printLine(`.map((e) => ${toIsoString('e', isNullable)})`);
          } else {
            sb += printLine(`.map((e) => ${buildToJsonClass('e', isNullable, input)})`);
          }
          sb += Array.from(result).map(_ => printLine('.toList())')).slice(0, -1).join('');
          sb += printLine('.toList(),');
        } else {
          sb += printLine(`'${typeDef.jsonKey}': ${thisKey}`);
          sb += Array.from(result).map(_ => printLine('?.map((e) => e')).slice(0, -1).join('');
          if (typeDef.isDate) {
            sb += printLine(`?.map((e) => ${toIsoString('e')})`);
          } else {
            sb += printLine(`?.map((e) => ${buildToJsonClass('e', false, input)})`);
          }
          sb += Array.from(result).map(_ => printLine('?.toList())')).slice(0, -1).join('');
          sb += printLine('?.toList(),');
        }
      } else {
        if (input.nullSafety) {
          const isNullable = typeDef.nullable;
          if (typeDef.isDate) {
            sb = `'${typeDef.jsonKey}': ${thisKey}?.map((e) => ${toIsoString('e', isNullable)}).toList(),`;
          } else {
            sb = `'${typeDef.jsonKey}': ${thisKey}?.map((e) => ${buildToJsonClass('e', isNullable, input)}).toList(),`;
          }
        } else {
          if (typeDef.isDate) {
            sb = `'${typeDef.jsonKey}': ${thisKey}?.map((e) => ${toIsoString('e')})?.toList(),`;
          } else {
            sb = `'${typeDef.jsonKey}': ${thisKey}?.map((e) => ${buildToJsonClass('e', false, input)})?.toList(),`;
          }
        }
      }
    } else {
      // Class
      const isNullable = input.nullSafety && !typeDef.nullable;
      if (typeDef.isDate) {
        sb = `'${typeDef.jsonKey}': ${toIsoString(thisKey, isNullable)},`;
      } else {
        sb = `'${typeDef.jsonKey}': ${buildToJsonClass(thisKey, isNullable, input)},`;
      }
    }
  }
  return sb;
};

export function jsonParseValue(
  key: string,
  typeDef: TypeDefinition,
  input: Input
) {
  const jsonValue = valueFromJson(key, input);
  const nullable = questionMark(input, typeDef);
  const isDouble = typeDef.type === 'double';
  const dafaultVal = typeDef.defaultValue
    ? `${isDouble ? '' : '?'} ?? ${defaultValue(typeDef, input.nullSafety, false).replace(/=|const/gi, '').trim()}`
    : '';
  const IfNull = `${includeIfNull(jsonValue, input)}`;
  let formatedValue = '';

  if (typeDef.isPrimitive) {
    const required = typeDef.required && input.avoidDynamicTypes ? '!' : '';

    if (typeDef.isDate) {
      formatedValue = jsonParseClass(key, typeDef, input);
    } else {
      if (!typeDef.nullable && !typeDef.isList) {
        formatedValue = `${jsonValue}`;
      } if (isDouble) {
        const nullableDouble = input.nullSafety && !typeDef.required ? '?' : '';
        formatedValue = `${IfNull}(${jsonValue}${required} as num${nullableDouble})${nullableDouble}.toDouble()` + dafaultVal;
      } else {
        formatedValue = `${IfNull}${jsonValue}${required} as ${typeDef.type}` + nullable + dafaultVal;
      }
    }
  } else {
    formatedValue = jsonParseClass(key, typeDef, input);
  }
  return formatedValue;
}

export function toJsonExpression(
  typeDef: TypeDefinition,
  privateField: boolean,
  input: Input,
): string {
  const fieldKey = typeDef.getName(privateField);
  const thisKey = `${fieldKey}`;
  if (typeDef.isPrimitive) {
    if (typeDef.isDate) {
      return toJsonClass(typeDef, privateField, input);
    } else {
      return `'${typeDef.jsonKey}': ${thisKey},`;
    }
  } else {
    return toJsonClass(typeDef, privateField, input);
  }
}

const buildToJsonClass = (expression: string, nullSafety: boolean = false, input: Input): string => {
  return nullSafety ?
    `${expression}.to${suffix(input)}()` :
    `${expression}?.to${suffix(input)}()`;
};

const buildParseClass = (className: string, expression: string, typeDef: TypeDefinition, input: Input): string => {
  const _suffix = input.fromAndToSuffix;
  const suffix = input.jsonCodecs && _suffix.toLowerCase() === 'json' ? 'Map' : _suffix;
  const name = pascalCase(className).replace(/_/g, '');
  const bangOperator = jsonMapType(input).match('Object?') && !typeDef.isList ? '!' : '';
  return `${name}.from${suffix}(${expression}${bangOperator} as ${jsonMapType(input)})`;
};

/**
 * DateTime parse function.
 * @param {string} expression specified value.
 * @param {boolean} clean if it is true then returns without without argument type.
 * @returns a string "DateTime.parse(expression)".
 */
const parseDateTime = (expression: string, withoutTypeCast: boolean = false, typeDef: TypeDefinition, input: Input): string => {
  const bangOperator = input.nullSafety && input.avoidDynamicTypes && !typeDef.isList ? '!' : '';
  const withArgumentType = `DateTime.parse(${expression}${bangOperator} as String)`;
  const withoutArgumentType = `DateTime.parse(${expression})`;
  return withoutTypeCast ? withoutArgumentType : withArgumentType;
};

const toIsoString = (expression: string, nullSafety: boolean = false): string => {
  const withNullCheck = `${expression}?.toIso8601String()`;
  const withoutNullCheck = `${expression}.toIso8601String()`;
  return nullSafety ? withoutNullCheck : withNullCheck;
};

export class Dependency {
  name: string;
  typeDef: TypeDefinition;

  constructor(name: string, typeDef: TypeDefinition) {
    this.name = name;
    this.typeDef = typeDef;
  }

  get className(): string {
    return camelCase(this.name);
  }
}

export class ClassDefinition {
  private _name: string;
  private _path: string;
  private _privateFields: boolean;
  private nameEnhancement: string = '';
  fields: Dependency[] = [];

  constructor(model: ClassNameModel, privateFields = false) {
    this._name = pascalCase(model.className);
    this._path = snakeCase(model.className);
    this.nameEnhancement = model.enhancement;
    this._privateFields = privateFields;
  }

  /** A class name. */
  get name() {
    return this._name;
  }

  /**
   * Update class name.
   * @param name a class name.
   */
  updateName(name: string) {
    this._name = pascalCase(name);
  }

  /** A class that converted back to the value 
   * @returns as object.
  */
  get value() {
    const keys = this.fields.map((k) => k.name);
    const values = this.fields.map((v) => v.typeDef.value);
    return _.zipObject(keys, values);
  }

  /** A path used for file names. */
  get path() {
    return this._path;
  }

  /**
   * Check if has value.
   * From the nested array will compare array object.
   * @param other value to compare.
   * @returns true if has value.
   */
  hasValue(other: any): boolean {
    if (Array.isArray(other)) {
      return _.isEqual(this.value, extractor(other));
    } else {
      return _.isEqual(this.value, other);
    }
  }

  /**
   * Check if the path exists.
   * @param {string} path a path to compare.
   * @returns true if path exists.
   */
  hasPath(path: string): boolean {
    return this._path === path;
  }

  /**
   * Check if a field has identical keys and equal by type.
   * From the nested array will compare array object.
   * * For checking the exact value, use `hasValue` instead.
   * @param other value to compare.
   * @returns boolean.
   */
  hasEqualField(other: any): boolean {
    if (Array.isArray(other)) {
      return equalByType(this.value, extractor(other));
    } else {
      return equalByType(this.value, other);
    }
  }

  /**
   * Path must contain the same import name as in the TypeDefinition.
   * @param {string} path a new name for path.
   */
  updatePath(path: string) {
    this._path = snakeCase(path);
  }

  get privateFields() {
    return this._privateFields;
  }

  get dependencies(): Dependency[] {
    var dependenciesList = new Array<Dependency>();
    for (const value of this.fields) {
      if (!value.typeDef.isPrimitive) {
        dependenciesList.push(new Dependency(value.name, value.typeDef));
      }
    }
    return dependenciesList;
  }

  private getFields(callbackfn: (typeDef: TypeDefinition, key: string) => any): TypeDefinition[] {
    return this.fields.map((v) => callbackfn(v.typeDef, v.name));
  }

  has = (other: ClassDefinition): boolean => {
    var otherClassDef: ClassDefinition = other;
    return this.isSubsetOf(otherClassDef) && otherClassDef.isSubsetOf(this)
      ? true
      : false;
  };

  private isSubsetOf = (other: ClassDefinition): boolean => {
    const keys = this.fields;
    const len = keys.length;
    for (let i = 0; i < len; i++) {
      var otherTypeDef = other.fields[i].typeDef;
      if (otherTypeDef !== undefined) {
        const typeDef = keys[i];
        if (!_.isEqual(typeDef, otherTypeDef)) {
          return false;
        }
      } else {
        return false;
      }
    }
    return true;
  };

  hasField(otherField: TypeDefinition) {
    return (
      this.fields.filter(
        (k) => _.isEqual(k.typeDef, otherField)
      ) !== null
    );
  }

  addField(name: string, typeDef: TypeDefinition) {
    this.fields.push(new Dependency(name, typeDef));
  }

  private addType(typeDef: TypeDefinition, input: Input) {
    const isDynamic = typeDef.type?.match('dynamic') && !typeDef.isList;

    return isDynamic
      ? typeDef.type
      : typeDef.type + questionMark(input, typeDef);

  }

  private fieldList(input: Input): string {
    return this.getFields((f) => {
      const fieldName = f.getName(this._privateFields);
      var sb = '\t';
      if (input.isImmutable) {
        sb += this.finalKeyword(true);
      }
      sb += this.addType(f, input) + ` ${fieldName};`;
      return sb;
    }).join('\n');
  }

  private fieldListCodeGen(input: Input): string {
    const final = input.isImmutable ? this.finalKeyword(true) : '';
    let sb = '';

    for (const f of this.fields.map((v) => v.typeDef)) {
      const fieldName = f.getName(this._privateFields);
      const jsonKey = jsonKeyAnnotation(f.name, f.jsonKey);

      if (jsonKey.length) {
        sb += '\t' + jsonKey + '\n';
      }

      sb += '\t' + final + this.addType(f, input) + ` ${fieldName};` + '\n';
    }

    return sb;
  }

  /**
   * Advanced abstract class with immutable values and objects.
   * @param {Input} input user input.
   */
  private freezedField(input: Input): string {
    var sb = '';
    const nonConstatValue = this.fields.some((f) => {
      return f.typeDef.isDate && !f.typeDef.isList && f.typeDef.defaultValue;
    });
    const privatConstructor = input.nullSafety && nonConstatValue
      ? printLine(`${this.name}._();`, 2, 1)
      : '';
    sb += printLine('@freezed');
    sb += printLine(`${input.nullSafety ? '' : 'abstract '}class ${this.name} with `, 1);
    sb += printLine(`_$${this.name} {`);
    sb += printLine(`factory ${this.name}({`, 1, 1);
    for (const typeDef of this.fields.map((v) => v.typeDef)) {
      const optional = 'optional' + pascalCase(typeDef.name);
      const fieldName = typeDef.getName(this._privateFields);
      const jsonKey = jsonKeyAnnotation(typeDef.name, typeDef.jsonKey);
      const defaultVal = defaultValue(typeDef, input.nullSafety, true);
      const required = requiredValue(typeDef.required, input.nullSafety);
      sb += printLine(jsonKey + required + defaultVal, 1, 2);

      if (typeDef.isDate && typeDef.defaultValue && !typeDef.isList) {
        sb += printLine(`${this.addType(typeDef, input)} ${optional},`);
      } else {
        sb += printLine(`${this.addType(typeDef, input)} ${fieldName},`);
      }
    };
    sb += printLine(`}) = _${this.name};`, 1, 1);
    sb += privatConstructor;
    sb += printLine(`${this.codeGenJsonParseFunc(input)}`);
    sb += defaultDateTime(this.fields, input);
    sb += printLine('}', 1);
    return sb;
  }

  /**
   * Returns a list of props that equatable needs to work properly.
   * @param {Input} input user input.
   */
  private equatablePropList(input: Input): string {
    if (!input.equatable) { return ''; }
    const fields = Array.from(this.fields.values());
    const expressionBody = (): string => {
      let sb = '';
      sb += printLine('@override', 2, 1);
      sb += printLine(`List<Object${questionMark(input)}> get props => [`, 1, 1);
      for (let i = 0; i < fields.length; i++) {
        const separator = fields.length - 1 === i ? '];' : ', ';
        const f = fields[i];
        sb += `${f.typeDef.getName(this._privateFields)}`;
        sb += separator;
      }
      return sb;
    };

    const blockBody = (): string => {
      let sb = '';
      sb += printLine('@override', 2, 1);
      sb += printLine(`List<Object${questionMark(input)}> get props {`, 1, 1);
      sb += printLine('return [', 1, 2);
      for (let i = 0; i < fields.length; i++) {
        const f = fields[i];
        sb += printLine(`${f.typeDef.getName(this._privateFields)},`, 1, 4);
      }
      sb += printLine('];', 1, 2);
      sb += printLine('}', 1, 1);
      return sb;
    };

    var isShort = expressionBody().length < 87;

    return isShort ? expressionBody() : blockBody();

  }

  /**
   * Equatable toString method including all the given props.
   * @param {boolean} print the string to be printed or no.
   * @returns string block.
   */
  private stringify(print: boolean = false): string {
    if (!print) { return ''; }
    var sb = '';
    sb += printLine('@override', 2, 1);
    sb += printLine('bool get stringify => true;', 1, 1);
    return sb;
  }

  /**
   * Keyword "final" to mark that object are immutable.
   * @param immutable should print the keyword or not
   */
  private finalKeyword(immutable: boolean = false): string {
    return immutable ? 'final ' : '';
  }

  /**
   * Keyword "const" to mark that class or object are immutable.
   * @param immutable should print the keyword or not.
   */
  private constKeyword(immutable: boolean = false): string {
    return immutable ? 'const ' : '';
  }

  private dartImports(input: Input): string {
    return input.jsonCodecs ? "import 'dart:convert';\n\n" : '';
  };

  /**
   * All imports from the packages library.
   * @param {Input} input the input from the user.
   */
  private importsFromPackage(input: Input): string {
    var imports = '';
    const required = this.fields.some((f) => f.typeDef.required && !input.nullSafety);
    const listEquality = this.fields.some((f) => f.typeDef.isList && input.equality === 'Dart');
    // Sorted alphabetically for effective dart style.
    imports += input.equatable && !input.freezed
      ? "import 'package:equatable/equatable.dart';\n"
      : '';
    imports += input.immutable && !input.serializable || input.equality === 'Dart' // || required || listEquality
      ? "import 'package:collection/collection.dart';\n"
      : '';
    imports += input.serializable && !input.freezed
      ? 'import \'package:json_annotation/json_annotation.dart\';\n'
      : '';
    imports += input.freezed
      ? "import 'package:freezed_annotation/freezed_annotation.dart';\n"
      : '';

    if (imports.length === 0) {
      return imports;
    } else {
      return imports += '\n';
    }
  };

  private importsForParts(input: Input): string {
    var imports = '';
    imports += input.freezed ? `part '${this._path}${this.nameEnhancement}.freezed.dart';\n` : '';
    imports += input.generate ? `part '${this._path}${this.nameEnhancement}.g.dart';\n` : '';
    if (imports.length === 0) {
      return imports;
    } else {
      return imports += '\n';
    }
  }

  private importList(): string {
    let imports = '';
    const nameSet = new Set(this.fields.map((f) => f.typeDef.importName).sort());
    const names = [...nameSet];

    for (const name of names) {
      if (name !== null) {
        imports += `import '${name}${this.nameEnhancement}.dart';\n`;
      }
    }

    if (imports.length === 0) {
      return imports;
    } else {
      return imports += '\n';
    }
  }

  private gettersSetters(input: Input): string {
    return this.getFields((f) => {
      var publicName = f.getName(false);
      var privateName = f.getName(true);
      var sb = '';
      sb += '\t';
      sb += this.addType(f, input);
      sb += `get ${publicName} => ${privateName};\n\tset ${publicName}(`;
      sb += this.addType(f, input);
      sb += ` ${publicName}) => ${privateName} = ${publicName};`;
      return sb;
    }).join('\n');
  }

  private defaultPrivateConstructor(input: Input): string {
    var sb = '';
    sb += `\t${this.name}({`;
    var i = 0;
    var len = Array.from(this.fields.keys()).length - 1;
    this.getFields((f) => {
      var publicName = f.getName(false);
      sb += this.addType(f, input);
      sb += ` ${publicName}`;
      if (i !== len) {
        sb += ', ';
      }
      i++;
    });
    sb += '}) {\n';
    this.getFields((f) => {
      var publicName = f.getName(false);
      var privateName = f.getName(true);
      sb += `this.${privateName} = ${publicName};\n`;
    });
    sb += '}';
    return sb;
  }

  private defaultConstructor(input: Input): string {
    let constructor = '';
    const values = this.fields.map((v) => v.typeDef);
    const defaultDate = defaultDateTime(this.fields, input);
    const isDefaultDate = defaultDate.length > 0;
    const areConstant = (typeDef: TypeDefinition) => {
      return typeDef.isDate && !typeDef.isList && typeDef.defaultValue;
    };
    const hasConstant = values.some(areConstant);
    const expression = (lines: number, tabs: number): string => {
      let sb = '';
      sb += '\t';
      sb += hasConstant ? '' : this.constKeyword(input.isImmutable);
      sb += `${this.name}({`;
      values.forEach((f) => {
        const name = f.getName(this._privateFields);
        const isDefaultDate = f.isDate && f.defaultValue && !f.isList;
        const thisKeyword = isDefaultDate ? '' : `this.${name}`;
        const required = requiredValue(f.required, input.nullSafety);
        const defaultVal = defaultValue(f, input.nullSafety);
        const field = required + thisKeyword + defaultVal;
        sb += printLine(`${field}, `, lines, tabs);
      });
      sb += printLine('});', lines, tabs >= 1 ? 1 : 0);
      return sb;
    };
    const isShort = expression(0, 0).length < 78;
    if (isShort && !isDefaultDate) {
      // Expression body.
      constructor = expression(0, 0).replace(', });', '});');
    } else if (isDefaultDate) {
      // Force print block body for better format.
      // Block body with initialized non-constant values.
      constructor = expression(1, 2).replace('});', '})');
      constructor += printLine(`${defaultDate};`, 0, 1);
    } else {
      // Block body.
      constructor = expression(1, 2);
    }

    return constructor;
  }

  private jsonParseFunc(input: Input): string {
    const mapValue = input.jsonCodecs ? 'data' : 'json';
    const blockBody = (): string => {
      let sb = '';
      sb += printLine(`factory ${this.name}`, 2, 1);
      sb += printLine(`.from${suffix(input)}(${jsonMapType(input)} ${mapValue}) {`);
      sb += printLine(`return ${this.name}(\n`, 1, 2);
      sb += this.getFields((f, k) => {
        // Check forced type for only not primitive type.
        const key = k.match('.') && !f.isList && f.type && !f.isPrimitive ? f.type : k;
        return `\t\t\t${joinAsClass(f.getName(this._privateFields), jsonParseValue(key, f, input))}`;
      }).join('\n');
      sb += printLine(');', 1, 2);
      sb += printLine('}\n\n', 1, 1);
      return sb;
    };

    const expressionBody = (): string => {
      let sb = '';
      sb += printLine(`factory ${this.name}`, 2, 1);
      sb += printLine(`.from${suffix(input)}(${jsonMapType(input)} ${mapValue}) => `);
      sb += printLine(`${this.name}(\n`);
      sb += this.getFields((f, k) => {
        // Check forced type for only not primitive type.
        const key = k.match('.') && !f.isList && f.type && !f.isPrimitive ? f.type : k;
        return `\t\t\t\t${joinAsClass(f.getName(this._privateFields), jsonParseValue(key, f, input))}`;
      }).join('\n');
      sb += printLine(');', 1, 3);
      return sb;
    };

    const line = expressionBody().substring(0, expressionBody().indexOf('(\n') + 1);

    return line.length > 78 ? blockBody() : expressionBody();
  }

  private toJsonFunc(input: Input): string {
    const _suffix = input.fromAndToSuffix;
    const suffix = input.jsonCodecs && _suffix.toLowerCase() === 'json' ? 'Map' : _suffix;
    const blockBody = (): string => {
      let sb = '';
      sb += printLine(`${jsonMapType(input)} to${suffix}() {`, 2, 1);
      sb += printLine('return {', 1, 2);
      this.getFields((f) => {
        sb += printLine(`${toJsonExpression(f, this._privateFields, input)}`, 1, 3);
      });
      sb += printLine('};', 0, 2);
      sb += printLine('}', 1, 1);
      return sb;
    };

    const expressionBody = (): string => {
      var sb = '';
      sb += printLine(`${jsonMapType(input)} to${suffix}() => {`, 2, 1);
      this.getFields((f) => {
        sb += printLine(`${toJsonExpression(f, this._privateFields, input)}`, 1, 4);
      });
      sb += printLine('};', 1, 3);
      return sb;
    };

    const line = expressionBody().substring(0, expressionBody().indexOf('{\n') + 1);

    return line.length > 80 ? blockBody() : expressionBody();
  }

  /**
   * Generate function for json_serializable and freezed.
   * @param freezed force to generate expression body (required for freezed generator).
   */
  private codeGenJsonParseFunc(input: Input): string {
    const expressionBody = (): string => {
      let sb = '';
      sb += printLine(`factory ${this.name}.`, 2, 1);
      sb += 'fromJson(Map<String, dynamic> json) => ';
      sb += `_$${this.name}FromJson(json);`;
      return sb;
    };
    const blockBody = (): string => {
      let sb = '';
      sb += printLine(`factory ${this.name}.`, 2, 1);
      sb += 'fromJson(Map<String, dynamic> json) {';
      sb += printLine(`return _$${this.name}FromJson(json);`, 1, 2);
      sb += printLine('}', 1, 1);
      return sb;
    };
    return expressionBody().length > 78 && !input.freezed
      ? blockBody()
      : expressionBody();
  }

  private codeGenToJsonFunc(): string {
    const expressionBody = (): string => {
      let sb = '';
      sb += printLine('Map<String, dynamic> toJson() => _$', 0, 1);
      sb += `${this.name}ToJson(this);`;
      return sb;
    };
    const blockBody = () => {
      let sb = '';
      sb += printLine('Map<String, dynamic> toJson() {', 0, 1);
      sb += printLine(`return _$${this.name}ToJson(this);`, 1, 2);
      sb += printLine('}', 1, 1);
      return sb;
    };
    return expressionBody().length > 78
      ? blockBody()
      : expressionBody();
  }

  private decodeFromJson(input: Input): string {
    if (!input.jsonCodecs) { return ''; }

    const comment = `
  /// \`dart:convert\`
  ///
  /// Parses the string and returns the resulting Json object as [${this.name}].`;
    let sb = '';
    sb += printLine(comment, 1);
    sb += printLine(`factory ${this.name}.fromJson(String data) {`, 1, 1);
    sb += printLine(`return ${this.name}.from${suffix(input)}(json.decode(data) as ${jsonMapType(input)});`, 1, 2);
    sb += printLine('}', 1, 1);
    return sb;
  }

  private encodeToJson(input: Input): string {
    if (!input.jsonCodecs) { return ''; }

    const comment = `
  /// \`dart:convert\`
  ///
  /// Converts [${this.name}] to a JSON string.`;
    let sb = '';
    sb += printLine(comment);
    sb += printLine(`String toJson() => json.encode(to${suffix(input)}());`, 1, 1);
    return sb;
  }

  /**
   * Generate copyWith(); mehtod for easier work with immutable classes.
   * @param {Input} input user input.
   */
  private copyWithMethod(input: Input): string {
    if (!input.copyWith) { return ''; }
    const values = this.fields.map((v) => v.typeDef);
    var sb = '';
    sb += printLine(`${this.name} copyWith({`, 2, 1);
    // Constructor objects.
    for (const value of values) {
      sb += printLine(`${this.addType(value, input)} ${value.name},`, 1, 2);
    }
    sb += printLine('}) {', 1, 1);
    sb += printLine(`return ${this.name}(`, 1, 2);
    // Return constructor.
    for (const value of values) {
      sb += printLine(`${value.name}: ${value.name} ?? this.${value.name},`, 1, 3);
    }
    sb += printLine(');', 1, 2);
    sb += printLine('}', 1, 1);
    return sb;
  }

  /**
   * `toString()` method in Dart language.
   * @param {boolean} print the string to be printed or no.
   * @param toString method should be generated or not.
   */
  private toStringMethod(print: boolean = false): string {
    if (!print) { return ''; }
    const values = this.fields.map((f) => f.typeDef);
    const expressionBody = (): string => {
      let sb = '';
      sb += printLine('@override', 2, 1);
      sb += printLine('String toString() => ', 1, 1);
      sb += `'${this.name}(`;
      for (let i = 0; i < values.length; i++) {
        const isEnd = values.length - 1 === i;
        const name = values[i].name;
        const separator = isEnd ? ')\';' : ', ';
        sb += `${name}: $${name}`;
        sb += separator;
      }
      return sb;
    };

    const blockBody = (): string => {
      let sb = '';
      sb += printLine('@override', 2, 1);
      sb += printLine('String toString() {', 1, 1);
      sb += printLine(`return '${this.name}(`, 1, 2);
      for (let i = 0; i < values.length; i++) {
        const isEnd = values.length - 1 === i;
        const name = values[i].name;
        const separator = isEnd ? ')\';' : ', ';
        sb += `${name}: $${name}`;
        sb += separator;
      }
      sb += printLine('}', 1, 1);
      return sb;
    };
    var isShort = expressionBody().length < 90;
    return isShort ? expressionBody() : blockBody();
  }

  /**
   * Equality Operator to compare different instances of `Objects`.
   * @param {Input} input user input.
   */
  private equalityOperator(input: Input): string {
    if (input.equality !== 'Dart') { return ''; }

    const type = input.nullSafety ? 'Object' : 'dynamic';
    // const castType = input.nullSafety ? '' : ' as List';
    // const fields = this.fields.map((f) => f.typeDef).sort((a, b) => {
    //   return a.isList === b.isList ? 0 : a ? -1 : 1;
    // });
    let sb = '';
    // sb += printLine('@override', 2, 1);
    // sb += printLine(`bool operator ==(${type} other) {`, 1, 1);
    // sb += printLine('if (identical(other, this)) return true;', 1, 2);
    // sb += printLine(`if (other is! ${this.name}) return false;`, 1, 2);
    // sb += printLine('return ', 1, 2);
    // const printBlock = (lines: number = 1, tabs: number = 4): string => {
    //   let sb = '';
    //   for (let i = 0; i < fields.length; i++) {
    //     const isEnd = fields.length - 1 === i;
    //     const field = fields[i];
    //     const separator = isEnd ? ';' : ' &&';
    //     const _lines = i === 0 ? 0 : lines;
    //     const _tabs = i === 0 ? 0 : tabs;
    //     if (field.isList) {
    //       sb += printLine(`listEquals(other.${field.name}${castType}, ${field.name})`, _lines, _tabs);
    //       sb += separator;
    //     } else {
    //       sb += printLine(`other.${field.name} == ${field.name}`, _lines, _tabs);
    //       sb += separator;
    //     }
    //   }

    //   return sb;
    // };

    // if (printBlock().length < 76) {
    //   sb += printBlock(0, 0);
    // } else {
    //   sb += printBlock();
    // }

    // New test template...
    const typeCast = input.nullSafety ? '' : ' as Map';
    sb += printLine('@override', 2, 1);
    sb += printLine(`bool operator ==(${type} other) {`, 1, 1);
    sb += printLine('if (identical(other, this)) return true;', 1, 2);
    sb += printLine(`if (other is! ${this.name}) return false;`, 1, 2);
    sb += printLine('final mapEquals = const DeepCollectionEquality().equals;', 1, 2);
    sb += printLine(`return mapEquals(other.to${suffix(input)}()${typeCast}, to${suffix(input)}());`, 1, 2);
    // ent test template...
    sb += printLine('}', 1, 1);
    return sb;
  }

  private hashCode(input: Input): string {
    if (input.equality !== 'Dart') { return ''; }
    const fields = this.fields.map((f) => f.typeDef);
    const expressionBody = (): string => {
      let sb = '';
      sb += printLine('@override', 2, 1);
      sb += printLine('int get hashCode => ', 1, 1);
      fields.forEach((f, i, arr) => {
        const isEnd = arr.length - 1 === i;
        const separator = isEnd ? ';' : ' ^ ';
        sb += `${f.name}.hashCode`;
        sb += separator;
      });
      return sb;
    };
    const blockBody = (): string => {
      let sb = '';
      sb += printLine('@override', 2, 1);
      sb += printLine('int get hashCode =>', 1, 1);
      fields.forEach((f, i, arr) => {
        const isEnd = arr.length - 1 === i;
        const separator = isEnd ? ';' : ' ^';
        sb += printLine(`${f.name}.hashCode` + separator, 1, 3);
      });
      return sb;
    };
    const isShort = expressionBody().length < 91;
    return isShort ? expressionBody() : blockBody();
  }

  toCodeGenString(input: Input): string {
    var field = '';

    if (this.fields.length === 0) {
      field = emptyClass(this.name);
      return field;
    }

    if (input.freezed) {
      field += this.importsFromPackage(input);
      field += this.importList();
      field += this.importsForParts(input);
      field += this.freezedField(input);
      return field;
    } else {
      if (this._privateFields) {
        field += this.importsFromPackage(input);
        field += this.importList();
        field += this.importsForParts(input);
        field += '@JsonSerializable()\n';
        field += `class ${this.name}${input.equatable ? ' extends Equatable' : ''}; {\n`;
        if (input.sortConstructorsFirst) {
          field += this.defaultPrivateConstructor(input) + '\n\n';
          field += this.fieldListCodeGen(input);
        } else {
          field += this.fieldListCodeGen(input) + '\n';
          field += this.defaultPrivateConstructor(input);
        }
        field += this.toStringMethod(input.isAutoOrToStringMethod);
        field += this.gettersSetters(input) + '\n\n';
        field += this.codeGenJsonParseFunc(input) + '\n\n';
        field += this.codeGenToJsonFunc();
        field += this.copyWithMethod(input);
        field += this.equalityOperator(input);
        field += this.hashCode(input);
        field += this.stringify(input.isAutoOrStringify);
        field += this.equatablePropList(input);
        field += '\n}\n'; // close class
        return field;
      } else {
        field += this.importsFromPackage(input);
        field += this.importList();
        field += this.importsForParts(input);
        field += '@JsonSerializable()\n';
        field += `class ${this.name}${input.equatable ? ' extends Equatable' : ''} {\n`;
        if (input.sortConstructorsFirst) {
          field += this.defaultConstructor(input) + '\n\n';
          field += this.fieldListCodeGen(input);
        } else {
          field += this.fieldListCodeGen(input) + '\n';
          field += this.defaultConstructor(input);
        }
        field += this.toStringMethod(input.isAutoOrToStringMethod);
        field += this.codeGenJsonParseFunc(input) + '\n\n';
        field += this.codeGenToJsonFunc();
        field += this.copyWithMethod(input);
        field += this.equalityOperator(input);
        field += this.hashCode(input);
        field += this.stringify(input.isAutoOrStringify);
        field += this.equatablePropList(input);
        field += '\n}\n'; // close class
        return field;
      }
    }
  }

  toString(input: Input): string {
    var field = '';

    if (this.fields.length === 0) {
      field = emptyClass(this.name);
      return field;
    }

    if (this._privateFields) {
      field += this.dartImports(input);
      field += this.importsFromPackage(input);
      field += this.importList();
      field += this.importsForParts(input);
      field += `${input.immutable ? '@immutable\n' : ''}`;
      field += `class ${this.name}${input.equatable ? ' extends Equatable' : ''} {\n`;
      if (input.sortConstructorsFirst) {
        field += this.defaultPrivateConstructor(input) + '\n\n';
        field += this.fieldList(input);
      } else {
        field += this.fieldList(input) + '\n\n';
        field += this.defaultPrivateConstructor(input);
      }
      field += this.toStringMethod(input.isAutoOrToStringMethod);
      field += this.gettersSetters(input) + '\n\n';
      field += this.jsonParseFunc(input) + '\n\n';
      field += this.toJsonFunc(input);
      field += this.copyWithMethod(input);
      field += this.equalityOperator(input);
      field += this.hashCode(input);
      field += this.stringify(input.isAutoOrStringify);
      field += this.equatablePropList(input);
      field += '\n}\n'; // close class
      return field;
    } else {
      field += this.dartImports(input);
      field += this.importsFromPackage(input);
      field += this.importList();
      field += this.importsForParts(input);
      field += `${input.immutable ? '@immutable\n' : ''}`;
      field += `class ${this.name}${input.equatable ? ' extends Equatable' : ''} {\n`;
      if (input.sortConstructorsFirst) {
        field += this.defaultConstructor(input) + '\n\n';
        field += this.fieldList(input);
      } else {
        field += this.fieldList(input) + '\n\n';
        field += this.defaultConstructor(input);
      }
      field += this.toStringMethod(input.isAutoOrToStringMethod);
      field += this.jsonParseFunc(input);
      field += this.toJsonFunc(input);
      field += this.decodeFromJson(input);
      field += this.encodeToJson(input);
      field += this.copyWithMethod(input);
      field += this.equalityOperator(input);
      field += this.hashCode(input);
      field += this.stringify(input.isAutoOrStringify);
      field += this.equatablePropList(input);
      field += '\n}\n'; // close class
      return field;
    }
  }
}