# Copyright (c) 2013 Maurizio Casimirri <maurizio.cas@gmail.com>
# (https://github.com/mcasimir)

# 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.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#= if not building
fs                 = require('fs')
Path               = require('path')
mkdirp             = require('mkdirp')
_                  = require('lodash')
SourceTypeRegister = require('./source_type_register')
SourceReader       = require('./source_reader')
PathResolver       = require('./path_resolver')
Document           = require('./document')
getDefaults        = require('./defaults')
#= else
#= require './source_type_register.coffee'
#= require './source_reader.coffee'
#= require './path_resolver.coffee'
#= require './document.coffee'
#= require './defaults.coffee'
#= end

class Builder
  @defaults = getDefaults()

  constructor: (options = {}) ->
    options = _.extend({}, Builder.defaults, options)
    
    @types = new SourceTypeRegister()
    @compilers = {}
    @_addTypes(options.types)
    @reader     = options.reader or new SourceReader()
    
    libs        = options.libs or []
    @resolver   = options.resolver or new PathResolver(libs)
    @env        = options.env or {}
    @_reset()
    
  setEnv: (locals = {}) ->
    _.extend(@env, locals)

  _addTypes: (types = {}) ->
    types = _.cloneDeep(types or {})
    for srcName, type of types
      compileTo = type.to or {}
      for destName, compileFunc  of compileTo
        @compilers["#{srcName}>#{destName}"] = { compile: compileFunc }
      delete type.to
    
    @types.add(types)

  config: (options = {}) ->

    @_addTypes(options.types)
    @reader     = options.reader or @reader
    
    if options.libs
      libs        = options.libs
      @resolver   = options.resolver or new PathResolver(libs)

    @env        = options.env or @env

  raiseParserError: (msg, line, col, path) ->
    path ?= @including[@including.length - 1] or @buildingSrc
    e = new Error(msg)
    e.path = path
    e.line = line
    e.column = col
    e.type = "PreprocessorError"
    throw e

  compile: (from, to, content, srcPath) ->
    compiler = @compilers["#{from}>#{to}"]
    if compiler
      try
        compiler.compile(content, srcPath)
      catch e
        @raiseParserError("Compilation error attempting to compile: '#{srcPath}' from: '#{from}' to '#{to}', reason: #{e.message}", e.line, e.column, srcPath)
    else
      content

  parse: (path) ->
    
    unless Path.resolve(path) is path
      throw "Source path must be absolute: #{path}."

    resolved  = path
    baseDir   = Path.dirname(path)
    
    if resolved in @including
      throw new Error("Inclusion loop detected trying to include '#{resolved}' inside itself.")
    
    @including.push(resolved)
    src  = @reader.read(resolved)
    type = @types.byFileName(resolved)

    type.parser.symbols = @env
    thisBuilder = @
    type.parser.raiseParserError = (msg, line, col) ->
      thisBuilder.raiseParserError(msg, line, col)

    parsed = []
    try
      blocks = type.parser.parse(src)
      
      for includeBlock in blocks
        if typeof includeBlock is "string"
          if includeBlock.length > 0
            parsed.push(includeBlock)

        else if includeBlock.type is "include"
          @line = includeBlock.line
          @col  = includeBlock.column

          includePaths = @resolver.glob(includeBlock.path, [baseDir])
          
          for includePath in includePaths
            unless (includePath in @included) and not includeBlock.force
              subdoc = if includeBlock.skip
                subtype = @types.byFileName(includePath)
                new Document(@reader.read(includePath), subtype.name)
              else
                @parse(includePath)

              content = subdoc.content
              if content.length > 0
                compiled = @compile(subdoc.type, type.name, content, includePath)
                parsed.splice(parsed.length - 1, 0, compiled)
              
        
    catch e
      if e.type is "PreprocessorError"
        throw e
      else if e.name is "SyntaxError" and e.line
        @raiseParserError(e, e.line, e.column)
      else
        @raiseParserError(e, @line, @col)

    @included.push(resolved)
    @including.pop()

    
    new Document(parsed.join(""), type.name)

  _reset: () ->
    @including = []
    @included = []
    @line = 0
    @col  = 0
    @buildingSrc = null


  dependenciesOf: (relSrc) ->
    root = @dependencyTree(relSrc)
    deps = []

    walker =
      walk: (node) ->
        if node != root
          deps.push node.src
        for child in node.dependencies
          @walk(child)

    walker.walk(root)
    deps

  dependencyTree: (relSrc) ->
    src = Path.resolve(relSrc)
    @_reset()
    
    { 
      src: src,
      dependencies: @_calculateDepsTree(src)
    }
    

  _calculateDepsTree: (path) ->
    unless Path.resolve(path) is path
      throw "Source path must be absolute: #{path}."

    dependencies = []
    
    resolved  = path
    baseDir   = Path.dirname(path)
    
    if resolved in @including
      throw new Error("Inclusion loop detected trying to include '#{resolved}' inside itself.")
    
    @including.push(resolved)
    try
      
      src  = @reader.read(resolved)
      type = @types.byFileName(resolved)

      type.parser.symbols = @env
      thisBuilder = @
      type.parser.raiseParserError = (msg, line, col) ->
        thisBuilder.raiseParserError(msg, line, col)

      blocks = type.parser.parse(src)
      for includeBlock in blocks
        if includeBlock.type is "include"
          includePaths = @resolver.glob(includeBlock.path, [baseDir])
          for includePath in includePaths
            unless (includePath in @included) # always treated as a 'require'
              if includeBlock.skip
                dependencies.push {
                  src: includePath
                  dependencies: []
                }
              else
                dependencies.push {
                  src: includePath
                  dependencies: @_calculateDepsTree(includePath)
                }


      @included.push(resolved)

    catch e
      if e.type is "PreprocessorError"
        throw e
      else


    @including.pop()

    dependencies


  build: (relSrc, relDest, options={}) ->
    src = Path.resolve(relSrc)
    @_reset()
    @buildingSrc = src
    doc = @parse(src)
    content = doc.content

    options = if typeof relDest is "object"
      relDest
    else
      options

    relDest = if typeof relDest is "string"
      relDest
    else
      undefined

    if relDest?
      dest = Path.resolve(relDest)

      destType = @types.byFileName(dest).name
      code = @compile(doc.type, destType, content, src)

      mkdirp.sync(Path.dirname(dest))
      fs.writeFileSync(dest, code)

    else
      if options.destType?
        @compile(doc.type, options.destType, content, src)
      else
        
        content

#= if not building
module.exports = Builder
#= end
