'use strict';

const _ = require('lodash');
const wkx = require('wkx');

module.exports = BaseTypes => {
  const warn = BaseTypes.ABSTRACT.warn.bind(undefined, 'http://www.postgresql.org/docs/9.4/static/datatype.html');

  /**
   * Removes unsupported Postgres options, i.e., LENGTH, UNSIGNED and ZEROFILL, for the integer data types.
   *
   * @param {object} dataType The base integer data type.
   * @private
   */
  function removeUnsupportedIntegerOptions(dataType) {
    if (dataType._length || dataType.options.length || dataType._unsigned || dataType._zerofill) {
      warn(`PostgresSQL does not support '${dataType.key}' with LENGTH, UNSIGNED or ZEROFILL. Plain '${dataType.key}' will be used instead.`);
      dataType._length = undefined;
      dataType.options.length = undefined;
      dataType._unsigned = undefined;
      dataType._zerofill = undefined;
    }
  }

  /**
   * types:
   * {
   *   oids: [oid],
   *   array_oids: [oid]
   * }
   *
   * @see oid here https://github.com/lib/pq/blob/master/oid/types.go
   */

  BaseTypes.UUID.types.postgres = ['uuid'];
  BaseTypes.CIDR.types.postgres = ['cidr'];
  BaseTypes.INET.types.postgres = ['inet'];
  BaseTypes.MACADDR.types.postgres = ['macaddr'];
  BaseTypes.TSVECTOR.types.postgres = ['tsvector'];
  BaseTypes.JSON.types.postgres = ['json'];
  BaseTypes.JSONB.types.postgres = ['jsonb'];
  BaseTypes.TIME.types.postgres = ['time'];

  class DATEONLY extends BaseTypes.DATEONLY {
    _stringify(value, options) {
      if (value === Infinity) {
        return 'Infinity';
      }
      if (value === -Infinity) {
        return '-Infinity';
      }
      return super._stringify(value, options);
    }
    _sanitize(value, options) {
      if ((!options || options && !options.raw) && value !== Infinity && value !== -Infinity) {
        if (typeof value === 'string') {
          const lower = value.toLowerCase();
          if (lower === 'infinity') {
            return Infinity;
          }
          if (lower === '-infinity') {
            return -Infinity;
          }
        }
        return super._sanitize(value);
      }
      return value;
    }
    static parse(value) {
      if (value === 'infinity') {
        return Infinity;
      }
      if (value === '-infinity') {
        return -Infinity;
      }
      return value;
    }
  }

  BaseTypes.DATEONLY.types.postgres = ['date'];

  class DECIMAL extends BaseTypes.DECIMAL {
    static parse(value) {
      return value;
    }
  }

  // numeric
  BaseTypes.DECIMAL.types.postgres = ['numeric'];

  class STRING extends BaseTypes.STRING {
    toSql() {
      if (this._binary) {
        return 'BYTEA';
      }
      return super.toSql();
    }
  }

  BaseTypes.STRING.types.postgres = ['varchar'];

  class TEXT extends BaseTypes.TEXT {
    toSql() {
      if (this._length) {
        warn('PostgreSQL does not support TEXT with options. Plain `TEXT` will be used instead.');
        this._length = undefined;
      }
      return 'TEXT';
    }
  }

  BaseTypes.TEXT.types.postgres = ['text'];

  class CITEXT extends BaseTypes.CITEXT {
    static parse(value) {
      return value;
    }
  }

  BaseTypes.CITEXT.types.postgres = ['citext'];

  class CHAR extends BaseTypes.CHAR {
    toSql() {
      if (this._binary) {
        return 'BYTEA';
      }
      return super.toSql();
    }
  }

  BaseTypes.CHAR.types.postgres = ['char', 'bpchar'];

  class BOOLEAN extends BaseTypes.BOOLEAN {
    toSql() {
      return 'BOOLEAN';
    }
    _sanitize(value) {
      if (value !== null && value !== undefined) {
        if (Buffer.isBuffer(value) && value.length === 1) {
          // Bit fields are returned as buffers
          value = value[0];
        }
        if (typeof value === 'string') {
          // Only take action on valid boolean strings.
          return value === 'true' || value === 't' ? true : value === 'false' || value === 'f' ? false : value;
        }
        if (typeof value === 'number') {
          // Only take action on valid boolean integers.
          return value === 1 ? true : value === 0 ? false : value;
        }
      }
      return value;
    }
  }

  BOOLEAN.parse = BOOLEAN.prototype._sanitize;

  BaseTypes.BOOLEAN.types.postgres = ['bool'];

  class DATE extends BaseTypes.DATE {
    toSql() {
      return 'TIMESTAMP WITH TIME ZONE';
    }
    validate(value) {
      if (value !== Infinity && value !== -Infinity) {
        return super.validate(value);
      }
      return true;
    }
    _stringify(value, options) {
      if (value === Infinity) {
        return 'Infinity';
      }
      if (value === -Infinity) {
        return '-Infinity';
      }
      return super._stringify(value, options);
    }
    _sanitize(value, options) {
      if ((!options || options && !options.raw) && !(value instanceof Date) && !!value && value !== Infinity && value !== -Infinity) {
        if (typeof value === 'string') {
          const lower = value.toLowerCase();
          if (lower === 'infinity') {
            return Infinity;
          }
          if (lower === '-infinity') {
            return -Infinity;
          }
        }
        return new Date(value);
      }
      return value;
    }
  }

  BaseTypes.DATE.types.postgres = ['timestamptz'];

  class TINYINT extends BaseTypes.TINYINT {
    constructor(length) {
      super(length);
      removeUnsupportedIntegerOptions(this);
    }
  }
  // int2
  BaseTypes.TINYINT.types.postgres = ['int2'];

  class SMALLINT extends BaseTypes.SMALLINT {
    constructor(length) {
      super(length);
      removeUnsupportedIntegerOptions(this);
    }
  }
  // int2
  BaseTypes.SMALLINT.types.postgres = ['int2'];

  class INTEGER extends BaseTypes.INTEGER {
    constructor(length) {
      super(length);
      removeUnsupportedIntegerOptions(this);
    }
  }
  INTEGER.parse = function parse(value) {
    return parseInt(value, 10);
  };

  // int4
  BaseTypes.INTEGER.types.postgres = ['int4'];

  class BIGINT extends BaseTypes.BIGINT {
    constructor(length) {
      super(length);
      removeUnsupportedIntegerOptions(this);
    }
  }
  // int8
  BaseTypes.BIGINT.types.postgres = ['int8'];

  class REAL extends BaseTypes.REAL {
    constructor(length) {
      super(length);
      removeUnsupportedIntegerOptions(this);
    }
  }
  // float4
  BaseTypes.REAL.types.postgres = ['float4'];

  class DOUBLE extends BaseTypes.DOUBLE {
    constructor(length) {
      super(length);
      removeUnsupportedIntegerOptions(this);
    }
  }
  // float8
  BaseTypes.DOUBLE.types.postgres = ['float8'];

  class FLOAT extends BaseTypes.FLOAT {
    constructor(length, decimals) {
      super(length, decimals);
      // POSTGRES does only support lengths as parameter.
      // Values between 1-24 result in REAL
      // Values between 25-53 result in DOUBLE PRECISION
      // If decimals are provided remove these and print a warning
      if (this._decimals) {
        warn('PostgreSQL does not support FLOAT with decimals. Plain `FLOAT` will be used instead.');
        this._length = undefined;
        this.options.length = undefined;
        this._decimals = undefined;
      }
      if (this._unsigned) {
        warn('PostgreSQL does not support FLOAT unsigned. `UNSIGNED` was removed.');
        this._unsigned = undefined;
      }
      if (this._zerofill) {
        warn('PostgreSQL does not support FLOAT zerofill. `ZEROFILL` was removed.');
        this._zerofill = undefined;
      }
    }
  }
  delete FLOAT.parse; // Float has no separate type in PG

  class BLOB extends BaseTypes.BLOB {
    toSql() {
      if (this._length) {
        warn('PostgreSQL does not support BLOB (BYTEA) with options. Plain `BYTEA` will be used instead.');
        this._length = undefined;
      }
      return 'BYTEA';
    }
    _hexify(hex) {
      // bytea hex format http://www.postgresql.org/docs/current/static/datatype-binary.html
      return `E'\\\\x${hex}'`;
    }
  }

  BaseTypes.BLOB.types.postgres = ['bytea'];

  class GEOMETRY extends BaseTypes.GEOMETRY {
    toSql() {
      let result = this.key;
      if (this.type) {
        result += `(${this.type}`;
        if (this.srid) {
          result += `,${this.srid}`;
        }
        result += ')';
      }
      return result;
    }
    static parse(value) {
      const b = Buffer.from(value, 'hex');
      return wkx.Geometry.parse(b).toGeoJSON({ shortCrs: true });
    }
    _stringify(value, options) {
      return `ST_GeomFromGeoJSON(${options.escape(JSON.stringify(value))})`;
    }
    _bindParam(value, options) {
      return `ST_GeomFromGeoJSON(${options.bindParam(value)})`;
    }
  }

  BaseTypes.GEOMETRY.types.postgres = ['geometry'];


  class GEOGRAPHY extends BaseTypes.GEOGRAPHY {
    toSql() {
      let result = 'GEOGRAPHY';
      if (this.type) {
        result += `(${this.type}`;
        if (this.srid) {
          result += `,${this.srid}`;
        }
        result += ')';
      }
      return result;
    }
    static parse(value) {
      const b = Buffer.from(value, 'hex');
      return wkx.Geometry.parse(b).toGeoJSON({ shortCrs: true });
    }
    _stringify(value, options) {
      return `ST_GeomFromGeoJSON(${options.escape(JSON.stringify(value))})`;
    }
    bindParam(value, options) {
      return `ST_GeomFromGeoJSON(${options.bindParam(value)})`;
    }
  }

  BaseTypes.GEOGRAPHY.types.postgres = ['geography'];

  let hstore;

  class HSTORE extends BaseTypes.HSTORE {
    constructor() {
      super();
      if (!hstore) {
        // All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
        hstore = require('./hstore');
      }
    }
    _value(value) {
      if (!hstore) {
        // All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
        hstore = require('./hstore');
      }
      return hstore.stringify(value);
    }
    _stringify(value) {
      return `'${this._value(value)}'`;
    }
    _bindParam(value, options) {
      return options.bindParam(this._value(value));
    }
    static parse(value) {
      if (!hstore) {
        // All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
        hstore = require('./hstore');
      }
      return hstore.parse(value);
    }
  }

  HSTORE.prototype.escape = false;

  BaseTypes.HSTORE.types.postgres = ['hstore'];

  class RANGE extends BaseTypes.RANGE {
    _value(values, options) {
      if (!Array.isArray(values)) {
        return this.options.subtype.stringify(values, options);
      }
      const valueInclusivity = [true, false];
      const valuesStringified = values.map((value, index) => {
        if (_.isObject(value) && Object.prototype.hasOwnProperty.call(value, 'value')) {
          if (Object.prototype.hasOwnProperty.call(value, 'inclusive')) {
            valueInclusivity[index] = value.inclusive;
          }
          value = value.value;
        }
        if (value === null || value === -Infinity || value === Infinity) {
          // Pass through "unbounded" bounds unchanged
          return value;
        }
        if (this.options.subtype.stringify) {
          return this.options.subtype.stringify(value, options);
        }
        return options.escape(value);
      });
      // Array.map does not preserve extra array properties
      valuesStringified.inclusive = valueInclusivity;
      return range.stringify(valuesStringified);
    }
    _stringify(values, options) {
      const value = this._value(values, options);
      if (!Array.isArray(values)) {
        return `'${value}'::${this.toCastType()}`;
      }
      return `'${value}'`;
    }
    _bindParam(values, options) {
      const value = this._value(values, options);
      if (!Array.isArray(values)) {
        return `${options.bindParam(value)}::${this.toCastType()}`;
      }
      return options.bindParam(value);
    }
    toSql() {
      return BaseTypes.RANGE.types.postgres.subtypes[this._subtype.toLowerCase()];
    }
    toCastType() {
      return BaseTypes.RANGE.types.postgres.castTypes[this._subtype.toLowerCase()];
    }
    static parse(value, options = { parser: val => val }) {
      return range.parse(value, options.parser);
    }
  }
  const range = require('./range');

  RANGE.prototype.escape = false;

  BaseTypes.RANGE.types.postgres = {
    subtypes: {
      integer: 'int4range',
      decimal: 'numrange',
      date: 'tstzrange',
      dateonly: 'daterange',
      bigint: 'int8range'
    },
    castTypes: {
      integer: 'int4',
      decimal: 'numeric',
      date: 'timestamptz',
      dateonly: 'date',
      bigint: 'int8'
    }
  };

  // TODO: Why are base types being manipulated??
  BaseTypes.ARRAY.prototype.escape = false;
  BaseTypes.ARRAY.prototype._value = function _value(values, options) {
    return values.map(value => {
      if (options && options.bindParam && this.type && this.type._value) {
        return this.type._value(value, options);
      }
      if (this.type && this.type.stringify) {
        value = this.type.stringify(value, options);

        if (this.type.escape === false) {
          return value;
        }
      }
      return options.escape(value);
    }, this);
  };
  BaseTypes.ARRAY.prototype._stringify = function _stringify(values, options) {
    let str = `ARRAY[${this._value(values, options).join(',')}]`;

    if (this.type) {
      const Utils = require('../../utils');
      let castKey = this.toSql();

      if (this.type instanceof BaseTypes.ENUM) {
        castKey = `${Utils.addTicks(
          Utils.generateEnumName(options.field.Model.getTableName(), options.field.fieldName),
          '"'
        ) }[]`;
      }

      str += `::${castKey}`;
    }

    return str;
  };
  BaseTypes.ARRAY.prototype._bindParam = function _bindParam(values, options) {
    return options.bindParam(this._value(values, options));
  };

  class ENUM extends BaseTypes.ENUM {
    static parse(value) {
      return value;
    }
  }

  BaseTypes.ENUM.types.postgres = [null];

  return {
    DECIMAL,
    BLOB,
    STRING,
    CHAR,
    TEXT,
    CITEXT,
    TINYINT,
    SMALLINT,
    INTEGER,
    BIGINT,
    BOOLEAN,
    DATE,
    DATEONLY,
    REAL,
    'DOUBLE PRECISION': DOUBLE,
    FLOAT,
    GEOMETRY,
    GEOGRAPHY,
    HSTORE,
    RANGE,
    ENUM
  };
};
