const Style = require("./style.js")
const Format = require("./format.js")
const stripAnsi = require("strip-ansi")

/**
 * Converts arrays of data into arrays of cell strings
 * @param {TtyTable.Config} config
 * @param {Array<Array<string>|object|TtyTable.Formatter>} inputData
 * @returns {Array<string>}
 */
module.exports.stringifyData = (config, inputData) => {
  const sections = {
    header: [],
    body: [],
    footer: []
  }
  const marginLeft = Array(config.marginLeft + 1).join(" ")
  const borderStyle = config.borderCharacters[config.borderStyle]
  const borders = []

  // support backwards compatibility cli-table's multiple constructor geometries
  // @TODO deprecate and support only a single format
  const constructorType = exports.getConstructorGeometry(inputData[0] || [], config)
  const rows = exports.coerceConstructorGeometry(config, inputData, constructorType)

  // when streaming values to tty-table, we don't want column widths to change
  // from one rows set to the next, so we save the first set of widths and reuse
  if (!global.columnWidths) {
    global.columnWidths = {}
  }

  if (global.columnWidths[config.tableId]) {
    config.table.columnWidths = global.columnWidths[config.tableId]
  } else {
    const formattedRows = rows.map((row, rowIndex) => {
      return row.map((cell, cellIndex) => {
        return exports.buildCell(config, cell, cellIndex, "body", rowIndex, rows, inputData, true)
      })
    })
    global.columnWidths[config.tableId] = config.table.columnWidths = Format.getColumnWidths(config, formattedRows)
  }

  // stringify header cells
  // hide header if no column names or if specified in config
  switch (true) {
    case (config.showHeader !== null && !config.showHeader): // explicitly false, hide
      sections.header = []
      break

    case (config.showHeader === true): // explicitly true, show
    case (!!config.table.header[0].find(obj => obj.value || obj.alias)): //  atleast one named column, show header
      sections.header = config.table.header.map(row => {
        return exports.buildRow(config, row, "header", null, rows, inputData)
      })
      break

    default: // no named columns, hide
      sections.header = []
  }

  // stringify body cells
  sections.body = rows.map((row, rowIndex) => {
    return exports.buildRow(config, row, "body", rowIndex, rows, inputData)
  })

  // stringify footer cells
  sections.footer = (config.table.footer instanceof Array && config.table.footer.length > 0) ? [config.table.footer] : []

  sections.footer = sections.footer.map(row => {
    return exports.buildRow(config, row, "footer", null, rows, inputData)
  })

  // apply borders
  // 0=top, 1=middle, 2=bottom
  for (let a = 0; a < 3; a++) {
    // add left border
    borders[a] = borderStyle[a].l

    // add joined borders for each column
    config.table.columnWidths.forEach((columnWidth, index, arr) => {
      // Math.max because otherwise columns 1 wide wont have horizontal border
      borders[a] += Array(Math.max(columnWidth, 2)).join(borderStyle[a].h)
      borders[a] += ((index + 1 < arr.length) ? borderStyle[a].j : "")
    })

    // add right border
    borders[a] += borderStyle[a].r

    // no trailing space on footer
    borders[a] = (a < 2) ? `${marginLeft + borders[a]}\n` : marginLeft + borders[a]
  }

  // top horizontal border
  let output = borders[0]

  // for each section (header,body,footer)
  Object.keys(sections).forEach((p, i) => {
    // for each row in the section
    while (sections[p].length) {
      const row = sections[p].shift()

      // if(row.length === 0) {break}

      row.forEach(line => {
        // vertical row borders
        output = `${output
          + marginLeft
          // left vertical border
          + borderStyle[1].v
          // join cells on vertical border
          + line.join(borderStyle[1].v)
          // right vertical border
          + borderStyle[1].v
          // end of line
        }\n`
      })

      // bottom horizontal row border
      switch (true) {
      // skip if end of body and no footer
        case (sections[p].length === 0
             && i === 1
             && sections.footer.length === 0):
          break

        // skip if end of footer
        case (sections[p].length === 0
             && i === 2):
          break

        // skip if compact
        case (config.compact && p === "body" && !row.empty):
          break

        // skip if border style is "none"
        case (config.borderStyle === "none" && config.compact):
          break

        default:
          output += borders[1]
      }
    }
  })

  // bottom horizontal border
  output += borders[2]

  const finalOutput = Array(config.marginTop + 1).join("\n") + output

  // record the height of the output
  config.height = finalOutput.split(/\r\n|\r|\n/).length

  return finalOutput
}

module.exports.buildRow = (config, row, rowType, rowIndex, rowData, inputData) => {
  let minRowHeight = 0

  // tag row as empty if empty, used for `compact` option
  if (row.length === 0 && config.compact) {
    row.empty = true
    return row
  }

  // force row to have correct number of columns
  const lengthDifference = config.table.columnWidths.length - row.length
  if (lengthDifference > 0) {
    // array (row) lacks elements, add until equal
    row = row.concat(Array.apply(null, new Array(lengthDifference)).map(() => null))
  } else if (lengthDifference < 0) {
    // array (row) has too many elements, remove until equal
    row.length = config.table.columnWidths.length
  }

  // convert each element in row to cell format
  row = row.map((elem, elemIndex) => {
    const cell = exports.buildCell(config, elem, elemIndex, rowType, rowIndex, rowData, inputData)
    minRowHeight = (minRowHeight < cell.length) ? cell.length : minRowHeight
    return cell
  })

  // apply top and bottom padding to row
  minRowHeight = (rowType === "header") ? minRowHeight
    : minRowHeight + (config.paddingBottom + config.paddingTop)

  const linedRow = Array.apply(null, { length: minRowHeight })
    .map(Function.call, () => [])

  row.forEach(function (cell, a) {
    const whitespace = Array(config.table.columnWidths[a]).join(" ")

    if (rowType === "body") {
      // add whitespace for top padding
      for (let i = 0; i < config.paddingTop; i++) {
        cell.unshift(whitespace)
      }

      // add whitespace for bottom padding
      for (let i = 0; i < config.paddingBottom; i++) {
        cell.push(whitespace)
      }
    }

    // a `row` is divided by columns (horizontally)
    // a `linedRow` becomes the row divided instead into an array of vertical lines
    // each nested line divided by columns
    for (let i = 0; i < minRowHeight; i++) {
      linedRow[i].push((typeof cell[i] !== "undefined")
        ? cell[i] : whitespace)
    }
  })

  return linedRow
}

module.exports.buildCell = (config, elem, columnIndex, rowType, rowIndex, rowData, inputData, dryRun = false) => {
  let cellValue = null

  const cellOptions = Object.assign(
    { reset: false },
    config,
    (rowType !== "header") ? config.columnSettings[columnIndex] : {},
    (typeof elem === "object") ? elem : {}
  )

  if (rowType === "header") {
    config.table.columns.push(cellOptions)
    cellValue = cellOptions.alias || cellOptions.value || ""
  } else {
    // set cellValue
    switch (true) {
      case (typeof elem === "undefined" || elem === null):
        // replace undefined/null elem values with placeholder
        cellValue = (config.errorOnNull) ? config.defaultErrorValue : config.defaultValue
        if (!Style.isColorEnabled()) {
          cellValue = stripAnsi(cellValue)
        }
        // @TODO add to elem defaults
        cellOptions.isNull = true
        break

      case (typeof elem === "object" && elem !== null && typeof elem.value !== "undefined"):
        cellValue = elem.value
        break

      case (typeof elem === "function"):
        cellValue = elem.bind({
          configure: function (object) {
            return Object.assign(cellOptions, object)
          },
          style: Style.style,
          resetStyle: Style.resetStyle
        })(
          cellValue,
          columnIndex,
          rowIndex,
          rowData,
          inputData
        )
        break

      default:
        // elem is assumed to be a scalar
        cellValue = elem
    }

    // run formatter
    if (rowType === "body" && typeof cellOptions.formatter === "function") {
      cellValue = cellOptions.formatter
        .bind({
          configure: function (object) {
            return Object.assign(cellOptions, object)
          },
          style: Style.style,
          resetStyle: Style.resetStyle
        })(
          cellValue,
          columnIndex,
          rowIndex,
          rowData,
          inputData
        )
    }

    if (dryRun) {
      return cellValue
    }
  }

  // colorize cellValue
  // we don't want the formatter to pass a styled cell value with ANSI codes
  // (in case user wants to do math or string operations to cell value), so
  // we apply default styles to the cell after it runs through the formatter
  // and omit those default styles if the user applied `this.resetStyle`
  if (!cellOptions.reset) {
    cellValue = Style.colorizeCell(cellValue, cellOptions, rowType)
  }

  // textwrap cellValue
  const { cell, innerWidth } = Format.wrapCellText(cellOptions, cellValue, columnIndex, cellOptions, rowType)

  if (rowType === "header") {
    config.table.columnInnerWidths.push(innerWidth)
  }

  return cell
}

/**
 * Check for a backwards compatible (cli-table) constructor
 */
module.exports.getConstructorGeometry = (row, config) => {
  let type

  // rows passed as an object
  if (typeof row === "object" && !(row instanceof Array)) {
    const keys = Object.keys(row)

    if (config.adapter === "automattic") {
      // detected cross table
      const key = keys[0]

      if (row[key] instanceof Array) {
        type = "automattic-cross"
      } else {
        // detected vertical table
        type = "automattic-vertical"
      }
    } else {
      // detected horizontal table
      type = "o-horizontal"
    }
  } else {
    // rows passed as an array
    type = "a-horizontal"
  }

  return type
}

/**
 * Coerce backwards compatible constructor styles
 */
module.exports.coerceConstructorGeometry = (config, rows, constructorType) => {
  let output = []
  switch (constructorType) {
    case ("automattic-cross"):
      // assign header styles to first column
      config.columnSettings[0] = config.columnSettings[0] || {}
      config.columnSettings[0].color = config.headerColor

      output = rows.map(obj => {
        const arr = []
        const key = Object.keys(obj)[0]
        arr.push(key)
        return arr.concat(obj[key])
      })
      break

    case ("automattic-vertical"):
      // assign header styles to first column
      config.columnSettings[0] = config.columnSettings[0] || {}
      config.columnSettings[0].color = config.headerColor

      output = rows.map(function (value) {
        const key = Object.keys(value)[0]
        return [key, value[key]]
      })
      break

    case ("o-horizontal"):
      // cell property names are specified in header columns
      if (config.table.header[0].length
        && config.table.header[0].every(obj => obj.value)) {
        output = rows.map(row => config.table.header[0]
          .map(obj => row[obj.value]))
      } // eslint-disable-line brace-style
      // no property names given, default to object property order
      else {
        output = rows.map(obj => Object.values(obj))
      }
      break

    case ("a-horizontal"):
      output = rows
      break

    default:
  }

  return output
}

// @TODO For rotating horizontal data into a vertical table
// assumes all rows are same length
// module.exports.verticalizeMatrix = (config, inputArray) => {
//
//   // grow to # arrays equal to number of columns in input array
//   let outputArray = []
//   let headers = config.table.columns
//
//   // create a row for each heading, and prepend the row
//   // with the heading name
//   headers.forEach(name => outputArray.push([name]))
//
//   inputArray.forEach(row => {
//     row.forEach((element, index) => outputArray[index].push(element))
//   })
//
//   return outputArray
// }
