/*
 * @license
 * Licensed under the <a href="http://www.opensource.org/licenses/mit-license.php">MIT license</a>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * Author Greg Kindel (twitter @gkindel), 2013
 */
/**
* CSV-JS - A Comma-Separated Values parser for JS
*
* Built to rfc4180 standard, with options for adjusting strictness:
*
*    - optional carriage returns for non-microsoft sources
*    - automatically type-cast numeric an boolean values
*    - relaxed mode which: ignores blank lines, ignores garbage following quoted tokens, does not enforce a consistent record length
*
* Adopted for UnityBase by pavel.mash
*
* @example
*
* var csv = require('@unitybase/base').csv;
* // simple
* var rows = csv.parse('one,two,three\nfour,five,six', ',')
* // rows equals [["one","two","three"],["four","five","six"]]
* // or read from file system
* var fs = require('fs'), f = fs.readFileSync('c:/csv.txt');
* var rows = csv.parse(f);
* for( var i =0; i < rows.length; i++){
*    console.log(rows[i]);
* }
*
* @module csv1
* @memberOf module:@unitybase/base
*/

const QUOTE = '"'
const CR = '\r'
const LF = '\n'
const COMMA = ';'
const SPACE = ' '
const TAB = '\t'

// implemented as a singleton because JS is single threaded
var CSV = {}
CSV.RELAXED = false
CSV.IGNORE_RECORD_LENGTH = false
CSV.IGNORE_QUOTES = false
CSV.LINE_FEED_OK = true
CSV.CARRIAGE_RETURN_OK = true
CSV.DETECT_TYPES = true
CSV.IGNORE_QUOTE_WHITESPACE = true
CSV.DEBUG = false
CSV.QUOTE = QUOTE
CSV.COMMA = COMMA

CSV.ERROR_EOF = 'UNEXPECTED_END_OF_FILE'
CSV.ERROR_CHAR = 'UNEXPECTED_CHARACTER'
CSV.ERROR_EOL = 'UNEXPECTED_END_OF_RECORD'
CSV.WARN_SPACE = 'UNEXPECTED_WHITESPACE' // not per spec, but helps debugging

// states
const PRE_TOKEN = 0
const MID_TOKEN = 1
const POST_TOKEN = 2
const POST_RECORD = 4
/**
 * <a href="http://www.ietf.org/rfc/rfc4180.txt">rfc4180</a> standard csv parse
 * with options for strictness and data type conversion.
 * By default, will automatically type-cast numeric an boolean values.
 *
 * @method parse
 * @param {String} str A CSV string
 * @param {String} [comma=";"] column separator
 * @return {Array} An array records, each of which is an array of scalar values.
 */
CSV.parse = function (str, comma) {
  CSV.COMMA = comma || ';'
  var result = CSV.result = []
  CSV.offset = 0
  CSV.str = str
  CSV.record_begin()

  CSV.debug('parse()', str)

  var c
  while (1) {
    // pull char
    c = str[CSV.offset++]
    CSV.debug('c', c)

    // detect eof
    if (c == null) {
      if (CSV.escaped) CSV.error(CSV.ERROR_EOF)

      if (CSV.record) {
        CSV.token_end()
        CSV.record_end()
      }

      CSV.debug('...bail', c, CSV.state, CSV.record)
      CSV.reset()
      break
    }

    if (CSV.record == null) {
      // if relaxed mode, ignore blank lines
      if (CSV.RELAXED && (c === LF || (c === CR && str[CSV.offset + 1] === LF))) {
        continue
      }
      CSV.record_begin()
    }

    // pre-token: look for start of escape sequence
    if (CSV.state === PRE_TOKEN) {
      if ((c === SPACE || c === TAB) && CSV.next_nonspace() === CSV.QUOTE) {
        if (CSV.RELAXED || CSV.IGNORE_QUOTE_WHITESPACE) {
          continue
        } else {
          // not technically an error, but ambiguous and hard to debug otherwise
          CSV.warn(CSV.WARN_SPACE)
        }
      }

      if (c === CSV.QUOTE && !CSV.IGNORE_QUOTES) {
        CSV.debug('...escaped start', c)
        CSV.escaped = true
        CSV.state = MID_TOKEN
        continue
      }
      CSV.state = MID_TOKEN
    }

    // mid-token and escaped, look for sequences and end quote
    if (CSV.state === MID_TOKEN && CSV.escaped) {
      if (c === CSV.QUOTE) {
        if (str[CSV.offset] === CSV.QUOTE) {
          CSV.debug('...escaped quote', c)
          CSV.token += CSV.QUOTE
          CSV.offset++
        } else {
          CSV.debug('...escaped end', c)
          CSV.escaped = false
          CSV.token_escaped = true
          CSV.state = POST_TOKEN
        }
      } else {
        CSV.token += c
        CSV.debug('...escaped add', c, CSV.token)
      }
      continue
    }

    // fall-through: mid-token or post-token, not escaped
    if (c === CR) {
      if (str[CSV.offset] === LF) {
        CSV.offset++
      } else if (!CSV.CARRIAGE_RETURN_OK) {
        CSV.error(CSV.ERROR_CHAR)
      }
      CSV.token_end()
      CSV.record_end()
    } else if (c === LF) {
      if (!(CSV.LINE_FEED_OK || CSV.RELAXED)) CSV.error(CSV.ERROR_CHAR)
      CSV.token_end()
      CSV.record_end()
    } else if (c === CSV.COMMA) {
      CSV.token_end()
    } else if (CSV.state === MID_TOKEN) {
      CSV.token += c
      CSV.debug('...add', c, CSV.token)
    } else if (c === SPACE || c === TAB) {
      if (!CSV.IGNORE_QUOTE_WHITESPACE) CSV.error(CSV.WARN_SPACE)
    } else if (!CSV.RELAXED) {
      CSV.error(CSV.ERROR_CHAR)
    }
  }
  return result
}

CSV.reset = function () {
  CSV.state = null
  CSV.token = null
  CSV.escaped = null
  CSV.record = null
  CSV.offset = null
  CSV.result = null
  CSV.str = null
}

CSV.next_nonspace = function () {
  let i = CSV.offset
  let c
  while (i < CSV.str.length) {
    c = CSV.str[i++]
    if (!(c === SPACE || c === TAB)) {
      return c
    }
  }
  return null
}

CSV.record_begin = function () {
  CSV.escaped = false
  CSV.record = []
  CSV.token_begin()
  CSV.debug('record_begin')
}

CSV.record_end = function () {
  CSV.state = POST_RECORD
  if (!(CSV.IGNORE_RECORD_LENGTH || CSV.RELAXED) &&
    (CSV.result.length > 0) && CSV.record.length !== CSV.result[0].length) {
    CSV.error(CSV.ERROR_EOL)
  }
  CSV.result.push(CSV.record)
  CSV.debug('record end', CSV.record)
  CSV.record = null
}

CSV.resolve_type = function (token) {
  if (token.match(/^[+-]?\d+(\.\d+)?$/)) {
    token = parseFloat(token)
  } else if (token.match(/^true|false$/i)) {
    token = Boolean(token.match(/true/i))
  } else if (token === 'undefined') {
    token = undefined
  } else if (token === 'null') {
    token = null
  }
  return token
}

CSV.token_begin = function () {
  CSV.state = PRE_TOKEN
  // considered using array, but http://www.sitepen.com/blog/2008/05/09/string-performance-an-analysis/
  CSV.token = ''
}

CSV.token_end = function () {
  if (CSV.DETECT_TYPES && !CSV.token_escaped) {
    CSV.token = CSV.resolve_type(CSV.token)
  }
  CSV.token_escaped = false
  CSV.record.push(CSV.token)
  CSV.debug('token end', CSV.token)
  CSV.token_begin()
}

CSV.debug = function () {
  if (CSV.DEBUG) console.log(arguments)
}

CSV.dump = function (msg) {
  return [
    msg, 'at char', CSV.offset, ':',
    CSV.str.substr(CSV.offset - 50, 50)
      .replace(/\r/mg, '\\r')
      .replace(/\n/mg, '\\n')
      .replace(/\t/mg, '\\t')
  ].join(' ')
}

CSV.error = function (err) {
  const msg = CSV.dump(err)
  CSV.reset()
  throw new Error(msg)
}

CSV.warn = function (err) {
  var msg = CSV.dump(err)
  try {
    console.warn(msg)
    return
  } catch (e) {}

  try {
    console.log(msg)
  } catch (e) {}
}

module.exports = CSV