var postcss = require('postcss')
var parser = require('postcss-selector-parser')

function parse (str, rule) {
  var nodes
  var saver = parser(function (parsed) {
    nodes = parsed
  })
  try {
    saver.processSync(str)
  } catch (e) {
    if (str.indexOf(':') !== -1) {
      throw rule ? rule.error('Missed semicolon') : e
    } else {
      throw rule ? rule.error(e.message) : e
    }
  }
  return nodes.at(0)
}

function replace (nodes, parent) {
  var replaced = false
  nodes.each(function (i) {
    if (i.type === 'nesting') {
      var clonedParent = parent.clone()
      if (i.value !== '&') {
        i.replaceWith(parse(i.value.replace('&', clonedParent.toString())))
      } else {
        i.replaceWith(clonedParent)
      }
      replaced = true
    } else if (i.nodes) {
      if (replace(i, parent)) {
        replaced = true
      }
    }
  })
  return replaced
}

function selectors (parent, child) {
  var result = []
  parent.selectors.forEach(function (i) {
    var parentNode = parse(i, parent)

    child.selectors.forEach(function (j) {
      var node = parse(j, child)
      var replaced = replace(node, parentNode)
      if (!replaced) {
        node.prepend(parser.combinator({ value: ' ' }))
        node.prepend(parentNode.clone())
      }
      result.push(node.toString())
    })
  })
  return result
}

function pickComment (comment, after) {
  if (comment && comment.type === 'comment') {
    after.after(comment)
    return comment
  } else {
    return after
  }
}

function atruleChilds (rule, atrule, bubbling) {
  var children = []
  atrule.each(function (child) {
    if (child.type === 'comment') {
      children.push(child)
    } else if (child.type === 'decl') {
      children.push(child)
    } else if (child.type === 'rule' && bubbling) {
      child.selectors = selectors(rule, child)
    } else if (child.type === 'atrule') {
      atruleChilds(rule, child, bubbling)
    }
  })
  if (bubbling) {
    if (children.length) {
      var clone = rule.clone({ nodes: [] })
      for (var i = 0; i < children.length; i++) {
        clone.append(children[i])
      }
      atrule.prepend(clone)
    }
  }
}

function processRule (rule, bubble, unwrap, preserveEmpty) {
  var unwrapped = false
  var after = rule

  rule.each(function (child) {
    if (child.type === 'rule') {
      unwrapped = true
      child.selectors = selectors(rule, child)
      after = pickComment(child.prev(), after)
      after.after(child)
      after = child
    } else if (child.type === 'atrule') {
      if (child.name === 'at-root') {
        unwrapped = true
        atruleChilds(rule, child, false)

        var nodes = child.nodes
        if (child.params) {
          nodes = postcss.rule({
            selector: child.params,
            nodes: nodes
          })
        }

        after.after(nodes)
        after = nodes
        child.remove()
      } else if (bubble[child.name]) {
        unwrapped = true
        atruleChilds(rule, child, true)
        after = pickComment(child.prev(), after)
        after.after(child)
        after = child
      } else if (unwrap[child.name]) {
        unwrapped = true
        atruleChilds(rule, child, false)
        after = pickComment(child.prev(), after)
        after.after(child)
        after = child
      }
    }
  })
  if (unwrapped && preserveEmpty !== true) {
    rule.raws.semicolon = true
    if (rule.nodes.length === 0) rule.remove()
  }
}

function atruleNames (defaults, custom) {
  var list = { }
  var i, name
  for (i = 0; i < defaults.length; i++) {
    list[defaults[i]] = true
  }
  if (custom) {
    for (i = 0; i < custom.length; i++) {
      name = custom[i].replace(/^@/, '')
      list[name] = true
    }
  }
  return list
}

module.exports = postcss.plugin('postcss-nested', function (opts) {
  if (!opts) opts = { }
  var bubble = atruleNames(['media', 'supports'], opts.bubble)
  var unwrap = atruleNames(['document', 'font-face', 'keyframes'], opts.unwrap)
  var preserveEmpty = opts ? opts.preserveEmpty : false

  var process = function (node) {
    node.each(function (child) {
      if (child.type === 'rule') {
        processRule(child, bubble, unwrap, preserveEmpty)
      } else if (child.type === 'atrule') {
        process(child)
      }
    })
  }
  return process
})