/* global define, require */
import { isArray } from '../utils';

let SourceNode;

try {
  /* istanbul ignore next */
  if (typeof define !== 'function' || !define.amd) {
    // We don't support this in AMD environments. For these environments, we assume that
    // they are running on the browser and thus have no need for the source-map library.
    let SourceMap = require('source-map');
    SourceNode = SourceMap.SourceNode;
  }
} catch (err) {
  /* NOP */
}

/* istanbul ignore if: tested but not covered in istanbul due to dist build  */
if (!SourceNode) {
  SourceNode = function(line, column, srcFile, chunks) {
    this.src = '';
    if (chunks) {
      this.add(chunks);
    }
  };
  /* istanbul ignore next */
  SourceNode.prototype = {
    add: function(chunks) {
      if (isArray(chunks)) {
        chunks = chunks.join('');
      }
      this.src += chunks;
    },
    prepend: function(chunks) {
      if (isArray(chunks)) {
        chunks = chunks.join('');
      }
      this.src = chunks + this.src;
    },
    toStringWithSourceMap: function() {
      return { code: this.toString() };
    },
    toString: function() {
      return this.src;
    }
  };
}

function castChunk(chunk, codeGen, loc) {
  if (isArray(chunk)) {
    let ret = [];

    for (let i = 0, len = chunk.length; i < len; i++) {
      ret.push(codeGen.wrap(chunk[i], loc));
    }
    return ret;
  } else if (typeof chunk === 'boolean' || typeof chunk === 'number') {
    // Handle primitives that the SourceNode will throw up on
    return chunk + '';
  }
  return chunk;
}

function CodeGen(srcFile) {
  this.srcFile = srcFile;
  this.source = [];
}

CodeGen.prototype = {
  isEmpty() {
    return !this.source.length;
  },
  prepend: function(source, loc) {
    this.source.unshift(this.wrap(source, loc));
  },
  push: function(source, loc) {
    this.source.push(this.wrap(source, loc));
  },

  merge: function() {
    let source = this.empty();
    this.each(function(line) {
      source.add(['  ', line, '\n']);
    });
    return source;
  },

  each: function(iter) {
    for (let i = 0, len = this.source.length; i < len; i++) {
      iter(this.source[i]);
    }
  },

  empty: function() {
    let loc = this.currentLocation || { start: {} };
    return new SourceNode(loc.start.line, loc.start.column, this.srcFile);
  },
  wrap: function(chunk, loc = this.currentLocation || { start: {} }) {
    if (chunk instanceof SourceNode) {
      return chunk;
    }

    chunk = castChunk(chunk, this, loc);

    return new SourceNode(
      loc.start.line,
      loc.start.column,
      this.srcFile,
      chunk
    );
  },

  functionCall: function(fn, type, params) {
    params = this.generateList(params);
    return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']);
  },

  quotedString: function(str) {
    return (
      '"' +
      (str + '')
        .replace(/\\/g, '\\\\')
        .replace(/"/g, '\\"')
        .replace(/\n/g, '\\n')
        .replace(/\r/g, '\\r')
        .replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4
        .replace(/\u2029/g, '\\u2029') +
      '"'
    );
  },

  objectLiteral: function(obj) {
    let pairs = [];

    Object.keys(obj).forEach(key => {
      let value = castChunk(obj[key], this);
      if (value !== 'undefined') {
        pairs.push([this.quotedString(key), ':', value]);
      }
    });

    let ret = this.generateList(pairs);
    ret.prepend('{');
    ret.add('}');
    return ret;
  },

  generateList: function(entries) {
    let ret = this.empty();

    for (let i = 0, len = entries.length; i < len; i++) {
      if (i) {
        ret.add(',');
      }

      ret.add(castChunk(entries[i], this));
    }

    return ret;
  },

  generateArray: function(entries) {
    let ret = this.generateList(entries);
    ret.prepend('[');
    ret.add(']');

    return ret;
  }
};

export default CodeGen;
