Q = require("q")
http = require("http")
url = require("url")
fs = require("fs")
xml2js = require("xml2js")
_ = require("underscore")
winston = require("winston")
util = require("util")

class ArgumentGenerator

  # @server is server
  # @cache is a type.prop, values cache, e.g. BedPlace.Oid: [BedPlace.1, BedPlace.2]
  # @premapped is a map of readymade conversions
  # @mappers is a list of mappers, if none are provide auto-discovery is set in motion
  constructor: (@server, @lookupConfiguration, @cache = {}, @premapped = {}, @mappers, @targeting) ->
    @version = "0.2.1"

    # find available mappers
    @parser = new xml2js.Parser({ ignoreAttrs: true })

    # if empty mappers list is supplied
    winston.info "loading mappers"
    if not @mappers?
      @mappers = {}
      for file in fs.readdirSync("ArgumentMappers") when file.match(/.*\.js/)
        do (file) =>
          m = require("./ArgumentMappers/#{file}").mapper
          @mappers[m.identifier] = m
          winston.info "installed mapper for '#{m.identifier}'"
            

    winston.info "loading targeting"
    if not @targeting?
      @targeting = {}
      for file in fs.readdirSync("ProfilerTargeting") when file.match(/.*\.js/)
        do (file) =>
          t = require("./ProfilerTargeting/#{file}").targeting
          @targeting[t.name] = t
          winston.info "installed targeting '#{t.name}'"
    

  # map from given arguments to ones matching the given server
  map: (method, action, component, componentVersion, endpoint, fun, args) ->

    cached = @premapped["#{method}.#{component}.#{componentVersion}.#{endpoint}.#{fun}.#{args}"]
    if cached? then return Q.fcall(->cached)

    # retrieve mapper for endpoint
    mapperId = [component, componentVersion, endpoint].filter((c) -> c?).join("/")
    mapper = @mappers[mapperId]

    # if no mapper for endpoint is found simple return url
    if not mapper? 
      winston.warn "No mapper for #{mapperId} found. Original args returned."
      return Q.fcall(->args)

    # get schema for function
    schema = mapper.schema(method, endpoint, action, fun, decodeURI(args))

    if not schema?
      winston.warn "No schema returned for '#{method} #{action} (#{component}/#{componentVersion}) #{endpoint}/#{fun}/#{args}'. Original args returned."
      return Q.fcall(->args)

    #  walk through schema and find arguments from 1) cache, 2) server
    promises = _.map(
      _.flatten(schema),
      (arg) =>
        switch arg.origin
          when "mapi"

            cached = @cache["#{arg.type}.#{arg.hql}.#{arg.depth}"] 
            value = if cached? then cached
            if not value?
              deferred = Q.defer()
              #deferred.promise.then (values) -> arg.newvalue = values
              #@cache["#{arg.type}.#{arg.hql}.#{arg.depth}"] = deferred.promise
              options =   
                host: @server
                port: 8080
                path: "/#{arg.type}?depth=#{arg.depth}&limit=200&hql=active=true" + (if arg.hql? then " AND "+arg.hql else "")
                auth: "spider:spider"
                headers: {
                  "User-Agent": "GLUTENFREE (AG-#{@version})"
                }
  
              # go through server
              http.get(options, (res) =>
                chunks = ""
                res.on("data", (chunk) -> 
                  chunks += chunk
                )
                res.on("end", () =>
                  console.log "get", options
                  # xml2js the shit out of this chunked mofo
                  @parser.parseString(
                    chunks, 
                    (err, data) =>
  
                      values = _.flatten(_.values(data.Collection))
  
                      # console.log "values", values
  
                      # save result in @cache
                      @cache["#{arg.type}.#{arg.hql}.#{arg.depth}"] = values
  
                      #  save result in arg.newval
                      arg.newvalue = values 
  
                      # done, resolve
                      deferred.resolve(arg)
                  )                  
                )
              ).on("error", (e) => 
                winston.error e
                deferred.reject("error when getting #{arg.type}.#{arg.hql}.#{arg.depth}")
              )
              # return
              return deferred.promise
            else # argument was cached
              arg.newvalue = value
              arg
          when "configuration", "conf"
            # load and match from configuration
            if @lookupConfiguration?
              newvalue = []
              for currentvalue in arg.currentvalue
                do (currentvalue) =>
                  v = @lookupConfiguration[arg.type][currentvalue]
                  if not v? # if no explicit mapping then pick random value 
                    v = _.first(_.shuffle(_.values(@lookupConfiguration[arg.type])))
                  newvalue.push(v)
              arg.newvalue = newvalue
            else
              arg.newvalue = arg.currentvalue
            # return arg
            arg
          when "fixed" # origin is not mapi
            arg.newvalue = arg.currentvalue
            arg
      )

    # wait for all promises to be fullfulled then let mapper generate url
    Q.allSettled(promises).then(
      =>
        # run through all resolved and pluck property
        for schematic in _.flatten(schema)
          do (schematic) ->
            if schematic.origin is "mapi"
              candidates = if schematic.filter? then _.filter(schematic.newvalue, (t) -> schematic.filter(t, schema)) else schematic.newvalue
              schematic.newvalue = _.first(_.shuffle(_.flatten(_.pluck(candidates,schematic.property))),schematic.currentvalue.length)

        url = mapper.applySchema(method, action, endpoint, fun, schema)
        @premapped["#{method}.#{component}.#{componentVersion}.#{endpoint}.#{fun}.#{args}"] = url
        console.log "url (pre-encode)", url
        encodeURI(url)
    )

exports.AG = ArgumentGenerator

# if run as main module
if require.main is module 

  argv = require('optimist')
    .usage("Usage: $0 -i [inputfile] -o [outputfile] -s [server] -t [targeting] -c [configuration]")
    .alias("s", "server")
    .demand(["i","o","s"])
    .alias("c", "configiration")
    .argv

  statsFile = if argv.i[0] is "/" then argv.i else "./#{argv.i}"
  stats = require(statsFile)


  lookupConf = 
    if argv.c? 
     if argv.c[0] is "/" then require(argv.c) else require("./#{argv.c}")
    else {}

  winston.info "creating ag"

  ag = new ArgumentGenerator(argv.s, lookupConf)

  winston.info "looping stats"

  # deep loop
  promises = []

  for id, info of stats.uniques
    do (id, info) ->
      info.newArgs = []
      promises.push(
        ag.map(info.method, info.action, info.component, info.componentVersion, info.endpoint, info.fun, info.args)
        .then(((newarg) -> info.newArgs.push newarg))
      )


  Q.all(promises)
  .then(
    -> 
      # targeting
      targeting = ag.targeting[argv.t]
      if not targeting?
        fs.writeFile(
          argv.o, 
          JSON.stringify(stats), 
          (err) -> if not err? then winston.info "raw output written to #{argv.o}" else winston.warn "error writing to #{argv.o}"
        )
      else
        targets = targeting.generate(stats)
        fs.writeFile(
          argv.o, 
          JSON.stringify(targets), 
          (err) -> if not err? then winston.info "targeting (#{targeting.name}) output written to #{argv.o}" else winston.warn "error writing to #{argv.o}"
        )
  )

