'use strict';

var parser = require('postcss-value-parser');
var list = require('postcss').list;

function convert(value) {
  if (value && value.length === 2 && value[0] === 'span' && parseInt(value[1], 10) > 0) {
    return [false, parseInt(value[1], 10)];
  }

  if (value && value.length === 1 && parseInt(value[0], 10) > 0) {
    return [parseInt(value[0], 10), false];
  }

  return [false, false];
}

function translate(values, startIndex, endIndex) {
  var startValue = values[startIndex];
  var endValue = values[endIndex];

  if (!startValue) {
    return [false, false];
  }

  var _convert = convert(startValue),
      start = _convert[0],
      spanStart = _convert[1];

  var _convert2 = convert(endValue),
      end = _convert2[0],
      spanEnd = _convert2[1];

  if (start && !endValue) {
    return [start, false];
  }

  if (spanStart && end) {
    return [end - spanStart, spanStart];
  }

  if (start && spanEnd) {
    return [start, spanEnd];
  }

  if (start && end) {
    return [start, end - start];
  }

  return [false, false];
}

function parse(decl) {
  var node = parser(decl.value);

  var values = [];
  var current = 0;
  values[current] = [];

  for (var _iterator = node.nodes, _isArray = Array.isArray(_iterator), _i = 0, _iterator = _isArray ? _iterator : _iterator[Symbol.iterator]();;) {
    var _ref;

    if (_isArray) {
      if (_i >= _iterator.length) break;
      _ref = _iterator[_i++];
    } else {
      _i = _iterator.next();
      if (_i.done) break;
      _ref = _i.value;
    }

    var i = _ref;

    if (i.type === 'div') {
      current += 1;
      values[current] = [];
    } else if (i.type === 'word') {
      values[current].push(i.value);
    }
  }

  return values;
}

function insertDecl(decl, prop, value) {
  if (value && !decl.parent.some(function (i) {
    return i.prop === '-ms-' + prop;
  })) {
    decl.cloneBefore({
      prop: '-ms-' + prop,
      value: value.toString()
    });
  }
}

// Track transforms

function prefixTrackProp(_ref2) {
  var prop = _ref2.prop,
      prefix = _ref2.prefix;

  return prefix + prop.replace('template-', '');
}

function transformRepeat(_ref3, _ref4) {
  var nodes = _ref3.nodes;
  var gap = _ref4.gap;

  var _nodes$reduce = nodes.reduce(function (result, node) {
    if (node.type === 'div' && node.value === ',') {
      result.key = 'size';
    } else {
      result[result.key].push(parser.stringify(node));
    }
    return result;
  }, {
    key: 'count',
    size: [],
    count: []
  }),
      count = _nodes$reduce.count,
      size = _nodes$reduce.size;

  if (gap) {
    var val = [];
    for (var i = 1; i <= count; i++) {
      if (gap && i > 1) {
        val.push(gap);
      }
      val.push(size.join());
    }
    return val.join(' ');
  }

  return '(' + size.join('') + ')[' + count.join('') + ']';
}

function prefixTrackValue(_ref5) {
  var value = _ref5.value,
      gap = _ref5.gap;

  var result = parser(value).nodes.reduce(function (nodes, node) {
    if (node.type === 'function' && node.value === 'repeat') {
      return nodes.concat({
        type: 'word',
        value: transformRepeat(node, { gap: gap })
      });
    }
    if (gap && node.type === 'space') {
      return nodes.concat({
        type: 'space',
        value: ' '
      }, {
        type: 'word',
        value: gap
      }, node);
    }
    return nodes.concat(node);
  }, []);

  return parser.stringify(result);
}

/**
 * Parse grid areas from declaration
 * @param Declaration [decl]
 * @return Array<String>
 */
function parseGridTemplateStrings(decl) {
  var template = parseTemplate({ decl: decl, gap: getGridGap(decl) });
  return Object.keys(template.areas);
}

/**
 * Walk through every grid-template(-areas)
 * declaration and try to find non-unique area names (duplicates)
 * @param  {Declaration} decl
 * @param  {Result} result
 * @return {void}
 */
function warnDuplicateNames(_ref6) {
  var decl = _ref6.decl,
      result = _ref6.result;

  var rule = decl.parent;
  var inMediaRule = !!getParentMedia(rule);
  var areas = parseGridTemplateStrings(decl);
  var root = decl.root();

  // stop if no areas found
  if (areas.length === 0) {
    return false;
  }

  root.walkDecls(/grid-template(-areas)?$/g, function (d) {
    var nodeRule = d.parent;
    var nodeRuleMedia = getParentMedia(nodeRule);
    var isEqual = rule.toString() === nodeRule.toString();
    var foundAreas = parseGridTemplateStrings(d);
    var maxIndex = Math.max(root.index(nodeRule), root.index(nodeRuleMedia));
    var isLookingDown = root.index(rule) < maxIndex;

    // skip node if no grid areas is found
    if (foundAreas.length === 0) {
      return true;
    }

    // abort if we outside media rule and walking below our rule
    // (we need to check ONLY the rules that are above)
    if (!inMediaRule && isLookingDown) {
      return false;
    }

    // abort if we're in the same rule
    if (isEqual) {
      return false;
    }

    // if we're inside media rule, we need to compare selectors
    if (inMediaRule) {
      var selectors = list.comma(nodeRule.selector);
      var selectorIsFound = selectors.some(function (sel) {
        if (sel === rule.selector) {
          // compare selectors as a comma list
          return true;
        } else if (nodeRule.selector === rule.selector) {
          // compare selectors as a whole (e.g. ".i, .j" === ".i, .j")
          return true;
        }
        return false;
      });

      // stop walking if we found the selector
      if (selectorIsFound) {
        return false;
      }
    }

    var duplicates = areas.filter(function (area) {
      return foundAreas.includes(area);
    });
    if (duplicates.length > 0) {
      var word = duplicates.length > 1 ? 'names' : 'name';
      decl.warn(result, ['', '  duplicate area ' + word + ' detected in rule: ' + rule.selector, '  duplicate area ' + word + ': ' + duplicates.join(', '), '  duplicate area names cause unexpected behavior in IE'].join('\n'));
      return false;
    }
    return undefined;
  });
  return undefined;
}

// Parse grid-template-areas

var DOTS = /^\.+$/;

function track(start, end) {
  return { start: start, end: end, span: end - start };
}

function getColumns(line) {
  return line.trim().split(/\s+/g);
}

function parseGridAreas(_ref7) {
  var rows = _ref7.rows,
      gap = _ref7.gap;

  return rows.reduce(function (areas, line, rowIndex) {
    if (gap.row) rowIndex *= 2;

    if (line.trim() === '') return areas;

    getColumns(line).forEach(function (area, columnIndex) {
      if (DOTS.test(area)) return;

      if (gap.column) columnIndex *= 2;

      if (typeof areas[area] === 'undefined') {
        areas[area] = {
          column: track(columnIndex + 1, columnIndex + 2),
          row: track(rowIndex + 1, rowIndex + 2)
        };
      } else {
        var _areas$area = areas[area],
            column = _areas$area.column,
            row = _areas$area.row;


        column.start = Math.min(column.start, columnIndex + 1);
        column.end = Math.max(column.end, columnIndex + 2);
        column.span = column.end - column.start;

        row.start = Math.min(row.start, rowIndex + 1);
        row.end = Math.max(row.end, rowIndex + 2);
        row.span = row.end - row.start;
      }
    });

    return areas;
  }, {});
}

// Parse grid-template

function testTrack(node) {
  return node.type === 'word' && /^\[.+\]$/.test(node.value);
}

function verifyRowSize(result) {
  if (result.areas.length > result.rows.length) {
    result.rows.push('auto');
  }
  return result;
}

function parseTemplate(_ref8) {
  var decl = _ref8.decl,
      gap = _ref8.gap;

  var gridTemplate = parser(decl.value).nodes.reduce(function (result, node) {
    var type = node.type,
        value = node.value;


    if (testTrack(node) || type === 'space') return result;

    // area
    if (type === 'string') {
      result = verifyRowSize(result);
      result.areas.push(value);
    }

    // values and function
    if (type === 'word' || type === 'function') {
      result[result.key].push(parser.stringify(node));
    }

    // devider(/)
    if (type === 'div' && value === '/') {
      result.key = 'columns';
      result = verifyRowSize(result);
    }

    return result;
  }, {
    key: 'rows',
    columns: [],
    rows: [],
    areas: []
  });

  return {
    areas: parseGridAreas({
      rows: gridTemplate.areas,
      gap: gap
    }),
    columns: prefixTrackValue({
      value: gridTemplate.columns.join(' '),
      gap: gap.column
    }),
    rows: prefixTrackValue({
      value: gridTemplate.rows.join(' '),
      gap: gap.row
    })
  };
}

// Insert parsed grid areas

/**
 * Get an array of -ms- prefixed props and values
 * @param  {Object} [area] area object with columm and row data
 * @param  {Boolean} [addRowSpan] should we add grid-column-row value?
 * @param  {Boolean} [addColumnSpan] should we add grid-column-span value?
 * @return {Array<Object>}
 */
function getMSDecls(area) {
  var addRowSpan = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
  var addColumnSpan = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;

  return [].concat({
    prop: '-ms-grid-row',
    value: String(area.row.start)
  }, area.row.span > 1 || addRowSpan ? {
    prop: '-ms-grid-row-span',
    value: String(area.row.span)
  } : [], {
    prop: '-ms-grid-column',
    value: String(area.column.start)
  }, area.column.span > 1 || addColumnSpan ? {
    prop: '-ms-grid-column-span',
    value: String(area.column.span)
  } : []);
}

function getParentMedia(parent) {
  if (parent.type === 'atrule' && parent.name === 'media') {
    return parent;
  } else if (!parent.parent) {
    return false;
  }
  return getParentMedia(parent.parent);
}

/**
 * Check grid-template(-areas) rules with the same selector for
 * -ms-grid-(row|column)-span values. If initial and compared values are
 * different - return an array of boolean values which need to be updated
 * @param  {Declaration} decl
 * @param  {Object} area area object with column and row data
 * @param  {String} areaName area name (e.g. "head")
 * @return {Array<Boolean, Boolean>}
 */
function shouldAddSpan(decl, area, areaName) {
  var root = decl.root();
  var rule = decl.parent;
  var overrideValues = [false, false];

  root.walkRules(rule.selector, function (node) {
    // abort if we are on the same rule
    if (rule.toString() === node.toString()) {
      return false;
    }

    node.walkDecls('grid-template', function (d) {
      var template = parseTemplate({ decl: d, gap: getGridGap(d) });
      var comparedArea = template.areas[areaName];
      if (area.row.span !== comparedArea.row.span) {
        overrideValues[0] = true;
      } else if (area.column.span !== comparedArea.column.span) {
        overrideValues[1] = true;
      }

      return undefined;
    });

    return undefined;
  });

  return overrideValues;
}

function insertAreas(areas, decl, result) {
  var missed = Object.keys(areas);

  var parentMedia = getParentMedia(decl.parent);
  var rules = [];
  var areasLength = Object.keys(areas).length;
  var areasCount = 0;

  decl.root().walkDecls('grid-area', function (gridArea) {
    var value = gridArea.value;
    var area = areas[value];

    missed = missed.filter(function (e) {
      return e !== value;
    });

    if (area && parentMedia) {
      // create new rule
      var rule = decl.parent.clone({
        selector: gridArea.parent.selector
      });
      rule.removeAll();

      // insert prefixed decls in new rule

      var _shouldAddSpan = shouldAddSpan(decl, area, value),
          addRowSpan = _shouldAddSpan[0],
          addColumnSpan = _shouldAddSpan[1];

      getMSDecls(area, addRowSpan, addColumnSpan).forEach(function (i) {
        return rule.append(Object.assign(i, {
          raws: {
            between: gridArea.raws.between
          }
        }));
      });

      rules.push(rule);
      areasCount++;
      if (areasCount === areasLength) {
        var next = gridArea.parent.next();

        if (next && next.type === 'atrule' && next.name === 'media' && next.params === parentMedia.params && next.first.type === 'rule' && next.first.selector && parentMedia.first.selector && /^-ms-/.test(next.first.first.prop)) return undefined;

        var areaParentMedia = getParentMedia(gridArea.parent);
        var areaMedia = parentMedia.clone().removeAll().append(rules);
        if (areaParentMedia) {
          areaParentMedia.after(areaMedia);
        } else {
          gridArea.parent.after(areaMedia);
        }
      }

      return undefined;
    }

    if (area) {
      gridArea.parent.walkDecls(/-ms-grid-(row|column)/, function (d) {
        d.remove();
      });

      // insert prefixed decls before grid-area
      getMSDecls(area).forEach(function (i) {
        return gridArea.cloneBefore(i);
      });
    }

    return undefined;
  });

  if (missed.length > 0) {
    decl.warn(result, 'Can not find grid areas: ' + missed.join(', '));
  }
}

// Gap utils

function getGridGap(decl) {
  var gap = {};

  // try to find gap
  var testGap = /^(grid-)?((row|column)-)?gap$/;
  decl.parent.walkDecls(testGap, function (_ref9) {
    var prop = _ref9.prop,
        value = _ref9.value;

    if (/^(grid-)?gap$/.test(prop)) {
      var _parser$nodes = parser(value).nodes,
          _parser$nodes$ = _parser$nodes[0],
          row = _parser$nodes$ === undefined ? {} : _parser$nodes$,
          _parser$nodes$2 = _parser$nodes[2],
          column = _parser$nodes$2 === undefined ? {} : _parser$nodes$2;


      gap.row = row.value;
      gap.column = column.value || row.value;
    }
    if (/^(grid-)?row-gap$/.test(prop)) gap.row = value;
    if (/^(grid-)?column-gap$/.test(prop)) gap.column = value;
  });

  return gap;
}

function warnGridGap(_ref10) {
  var gap = _ref10.gap,
      hasColumns = _ref10.hasColumns,
      decl = _ref10.decl,
      result = _ref10.result;

  var hasBothGaps = gap.row && gap.column;
  if (!hasColumns && (hasBothGaps || gap.column && !gap.row)) {
    delete gap.column;
    decl.warn(result, 'Can not impliment grid-gap without grid-tamplate-columns');
  }
}

module.exports = {
  parse: parse,
  translate: translate,
  parseTemplate: parseTemplate,
  parseGridAreas: parseGridAreas,
  insertAreas: insertAreas,
  insertDecl: insertDecl,
  prefixTrackProp: prefixTrackProp,
  prefixTrackValue: prefixTrackValue,
  getGridGap: getGridGap,
  warnGridGap: warnGridGap,
  warnDuplicateNames: warnDuplicateNames
};