
class SwaggerApi
  
  # Defaults
  url: "http://api.wordnik.com/v4/resources.json"
  debug: false
  basePath: null
  authorizations: null
  authorizationScheme: null
  info: null

  constructor: (url, options={}) ->
    # if url is a hash, assume only options were passed

    if url 
      if url.url
        options = url
      else
        @url = url
    else
      options = url
    @url = options.url if options.url?

    @supportedSubmitMethods = if options.supportedSubmitMethods? then options.supportedSubmitMethods else ['get']
    @success = options.success if options.success?
    @failure = if options.failure? then options.failure else ->
    @progress = if options.progress? then options.progress else ->
    @defaultHeaders = if options.headers? then options.headers else {}

    # Build right away if a callback was passed to the initializer
    @build() if options.success?

  build: ->
    @progress 'fetching resource list: ' + @url
    obj = 
      url: @url
      method: "get"
      headers: {}
      on:
        error: (response) =>
          if @url.substring(0, 4) isnt 'http'
            @fail 'Please specify the protocol for ' + @url
          else if error.status == 0
            @fail 'Can\'t read from server.  It may not have the appropriate access-control-origin settings.'
          else if error.status == 404
            @fail 'Can\'t read swagger JSON from '  + @url
          else
            @fail error.status + ' : ' + error.statusText + ' ' + @url
        response: (rawResponse) =>
          response = JSON.parse(rawResponse.content.data)
          @apiVersion = response.apiVersion if response.apiVersion?

          @apis = {}
          @apisArray = []
          @produces = response.produces
          @info = response.info if response.info?

          # if apis.operations exists, this is an api declaration as opposed to a resource listing
          isApi = false
          for api in response.apis
            if api.operations
              for operation in api.operations
                isApi = true

          if isApi
            newName = response.resourcePath.replace(/\//g,'')
            this.resourcePath = response.resourcePath

            res = new SwaggerResource response, this
            @apis[newName] = res
            @apisArray.push res
          else
            # The base path derived from url
            if response.basePath
              # support swagger 1.1, which has basePath
              @basePath = response.basePath
            else if @url.indexOf('?') > 0
              @basePath = @url.substring(0, @url.lastIndexOf('?'))
            else
              @basePath = @url

            for resource in response.apis
              res = new SwaggerResource resource, this
              @apis[res.name] = res
              @apisArray.push res
          if this.success
            this.success()
          this
          
    # apply authorizations
    e = {}
    if typeof window != 'undefined'
      e = window
    else
      e = exports
    e.authorizations.apply obj

    new SwaggerHttp().execute obj
    @

  # This method is called each time a child resource finishes loading
  # 
  selfReflect: ->
    return false unless @apis?
    for resource_name, resource of @apis
      return false unless resource.ready?

    @setConsolidatedModels()
    @ready = true
    @success() if @success?

  fail: (message) ->
    @failure message
    throw message

  # parses models in all apis and sets a unique consolidated list of models
  setConsolidatedModels: ->
    @modelsArray = []
    @models = {}
    for resource_name, resource of @apis
      for modelName of resource.models
        if not @models[modelName]?
          @models[modelName] = resource.models[modelName]
          @modelsArray.push resource.models[modelName]
    for model in @modelsArray
      model.setReferencedModels(@models)

  help: ->
    for resource_name, resource of @apis
      console.log resource_name
      for operation_name, operation of resource.operations
        console.log "  #{operation.nickname}"
        for parameter in operation.parameters
          console.log "    #{parameter.name}#{if parameter.required then ' (required)'  else ''} - #{parameter.description}"
    @
        
class SwaggerResource
  api: null
  produces: null
  consumes: null

  constructor: (resourceObj, @api) ->
    this.api = @api
    produces = []
    consumes = []

    @path = if @api.resourcePath? then @api.resourcePath else resourceObj.path

    @description = resourceObj.description

    # Extract name from path
    # '/foo/dogs.format' -> 'dogs'
    parts = @path.split("/")
    @name = parts[parts.length - 1].replace('.{format}', '')

    # set the basePath to be the one in API. If response from server for this resource
    # has a more specific one, we'll set it again there.
    @basePath = @api.basePath

    # We're going to store operations in a map (operations) and a list (operationsArray)
    @operations = {}
    @operationsArray = []

    # We're going to store models in a map (models) and a list (modelsArray)
    @modelsArray = []
    @models = {}

    if resourceObj.apis? and @api.resourcePath?
      # read resource directly from operations object
      @addApiDeclaration(resourceObj)

    else
      # read from server
      @api.fail "SwaggerResources must have a path." unless @path?

      # e.g."http://api.wordnik.com/v4/word.json"

      if @path.substring(0,4) == 'http'
        # user absolute path
        @url = @path.replace('{format}', 'json')
      else
        @url = @api.basePath + @path.replace('{format}', 'json')

      @api.progress 'fetching resource ' + @name + ': ' + @url
      obj = 
        url: @url
        method: "get"
        headers: {}
        on:
          error: (response) =>
            @api.fail "Unable to read api '" + @name + "' from path " + @url + " (server returned " + error.statusText + ")"
          response: (rawResponse) =>
            response = JSON.parse(rawResponse.content.data)
            @addApiDeclaration(response)

      # apply authorizations
      e = {}
      if typeof window != 'undefined'
        e = window
      else
        e = exports
      e.authorizations.apply obj

      new SwaggerHttp().execute obj

  addApiDeclaration: (response) ->
    if response.produces?
      @produces = response.produces
    if response.consumes?
      @consumes = response.consumes

    # If there is a basePath in response, use that or else use
    # the one from the api object
    if response.basePath? and response.basePath.replace(/\s/g,'').length > 0
      @basePath = response.basePath

    @addModels(response.models)

    # Instantiate SwaggerOperations and store them in the @operations map and @operationsArray
    if response.apis
      for endpoint in response.apis
        @addOperations(endpoint.path, endpoint.operations, response.consumes, response.produces)

    # Store a named reference to this resource on the parent object
    @api[this.name] = this

    # Mark as ready
    @ready = true

    # Now that this resource is loaded, tell the API to check in on itself
    @api.selfReflect()

  addModels: (models) ->
    if models?
      for modelName of models
        if not @models[modelName]?
          swaggerModel = new SwaggerModel(modelName, models[modelName])
          @modelsArray.push swaggerModel
          @models[modelName] = swaggerModel
      for model in @modelsArray
        model.setReferencedModels(@models)


  addOperations: (resource_path, ops, consumes, produces) ->
    if ops
      for o in ops
        consumes = @consumes
        produces = @produces

        if o.consumes?
          consumes = o.consumes
        else
          consumes = @consumes

        if o.produces?
          produces = o.produces
        else
          produces = @produces

        type = o.type || o.responseClass
        if(type is "array")
          ref = null
          if o.items
            ref = o.items["type"] || o.items["$ref"]
          type = "array[" + ref + "]"

        responseMessages = o.responseMessages
        method = o.method

        # support old httpMethod
        if o.httpMethod
          method = o.httpMethod

        # support old naming
        if o.supportedContentTypes
          consumes = o.supportedContentTypes

        # support old error responses
        if o.errorResponses
          responseMessages = o.errorResponses

        # sanitize the nickname
        o.nickname = @sanitize o.nickname

        op = new SwaggerOperation o.nickname, resource_path, method, o.parameters, o.summary, o.notes, type, responseMessages, this, consumes, produces
        @operations[op.nickname] = op
        @operationsArray.push op

  sanitize: (nickname) ->
    # allow only _a-zA-Z0-9
    op = nickname.replace /[\s!@#$%^&*()_+=\[{\]};:<>|./?,\\'""-]/g, '_'
    # trim multiple underscores to one
    op = op.replace /((_){2,})/g, '_'
    # ditch leading underscores
    op = op.replace /^(_)*/g, ''
    # ditch trailing underscores
    op = op.replace /([_])*$/g, ''
    op

  help: ->
    for operation_name, operation of @operations
      msg = "  #{operation.nickname}"
      for parameter in operation.parameters
        msg.concat("    #{parameter.name}#{if parameter.required then ' (required)'  else ''} - #{parameter.description}")
      msg


class SwaggerModel
  constructor: (modelName, obj) ->
    @name = if obj.id? then obj.id else modelName
    @properties = []
    for propertyName of obj.properties
      @properties.push new SwaggerModelProperty(propertyName, obj.properties[propertyName])

  # Set models referenced  bu this model
  setReferencedModels: (allModels) ->
    for prop in @properties
      type = prop.type || prop.dataType
      if allModels[type]?
        prop.refModel = allModels[type]
      else if prop.refDataType? and allModels[prop.refDataType]?
        prop.refModel = allModels[prop.refDataType]

  getMockSignature: (modelsToIgnore) ->
    propertiesStr = []
    for prop in @properties
      propertiesStr.push prop.toString()

    strong = '<span class="strong">';
    stronger = '<span class="stronger">';
    strongClose = '</span>';
    classOpen = strong + @name + ' {' + strongClose
    classClose = strong + '}' + strongClose
    returnVal = classOpen + '<div>' + propertiesStr.join(',</div><div>') + '</div>' + classClose

    # create the array if necessary and then add the current element
    if !modelsToIgnore
      modelsToIgnore = []
    modelsToIgnore.push(@)

    # iterate thru all properties and add models which are not in modelsToIgnore
    # modelsToIgnore is used to ensure that recursive references do not lead to endless loop
    # and that the same model is not displayed multiple times
    for prop in @properties
      if(prop.refModel? and (modelsToIgnore.indexOf(prop.refModel)) == -1)
        returnVal = returnVal + ('<br>' + prop.refModel.getMockSignature(modelsToIgnore))

    returnVal

  createJSONSample: (modelsToIgnore) ->
    result = {}
    modelsToIgnore = modelsToIgnore || [];
    modelsToIgnore.push(@name);
    for prop in @properties
      result[prop.name] = prop.getSampleValue(modelsToIgnore)
    result

class SwaggerModelProperty
  constructor: (@name, obj) ->
    @dataType = obj.type
    @isCollection  = @dataType && (@dataType.toLowerCase() is 'array' || @dataType.toLowerCase() is 'list' ||
      @dataType.toLowerCase() is 'set');
    @descr = obj.description
    @required = obj.required

    if obj.items?
      if obj.items.type? then @refDataType = obj.items.type
      if obj.items.$ref? then @refDataType = obj.items.$ref
    @dataTypeWithRef = if @refDataType? then (@dataType + '[' + @refDataType + ']') else @dataType
    if obj.allowableValues?
      @valueType = obj.allowableValues.valueType
      @values = obj.allowableValues.values
      if @values?
        @valuesString = "'" + @values.join("' or '") + "'"

  getSampleValue: (modelsToIgnore) ->
    if(@refModel? and (modelsToIgnore.indexOf(@refModel.name) is -1))
      result = @refModel.createJSONSample(modelsToIgnore)
    else
      if @isCollection
        result = @refDataType
      else
        result = @dataType
    if @isCollection then [result] else result

  toString: ->
    req = if @required then 'propReq' else 'propOpt'

    str = '<span class="propName ' + req + '">' + @name + '</span> (<span class="propType">' + @dataTypeWithRef + '</span>';
    if !@required
      str += ', <span class="propOptKey">optional</span>'

    str += ')';
    if @values?
      str += " = <span class='propVals'>['" + @values.join("' or '") + "']</span>"

    if @descr?
      str += ': <span class="propDesc">' + @descr + '</span>'

    str

# SwaggerOperation converts an operation into a method which can be executed directly
class SwaggerOperation
  constructor: (@nickname, @path, @method, @parameters=[], @summary, @notes, @type, @responseMessages, @resource, @consumes, @produces) ->
    @resource.api.fail "SwaggerOperations must have a nickname." unless @nickname?
    @resource.api.fail "SwaggerOperation #{nickname} is missing path." unless @path?
    @resource.api.fail "SwaggerOperation #{nickname} is missing method." unless @method?

    # Convert {format} to 'json'
    @path = @path.replace('{format}', 'json')
    @method = @method.toLowerCase()
    @isGetMethod = @method == "get"
    @resourceName = @resource.name

    # if void clear it
    console.log "model type: " + type
    if(@type?.toLowerCase() is 'void') then @type = undefined
    if @type?
      # set the signature of response class
      @responseClassSignature = @getSignature(@type, @resource.models)
      @responseSampleJSON = @getSampleJSON(@type, @resource.models)

    @responseMessages = @responseMessages || []

    for parameter in @parameters
      # Path params do not have a name, set the name to the path if name is n/a
      parameter.name = parameter.name || parameter.type || parameter.dataType

      type = parameter.type || parameter.dataType

      if(type.toLowerCase() is 'boolean')
        parameter.allowableValues = {}
        parameter.allowableValues.values = @resource.api.booleanValues

      parameter.signature = @getSignature(type, @resource.models)
      parameter.sampleJSON = @getSampleJSON(type, @resource.models)

      # Set allowableValue attributes
      if parameter.allowableValues?
        # Set isRange and isList flags on param
        if parameter.allowableValues.valueType == "RANGE"
          parameter.isRange = true
        else
          parameter.isList = true

        # Set a descriptive values on allowable values
        # This contains value and isDefault flag for each value
        if parameter.allowableValues.values?
          parameter.allowableValues.descriptiveValues = []
          for v in parameter.allowableValues.values
            if parameter.defaultValue? and parameter.defaultValue == v
              parameter.allowableValues.descriptiveValues.push {value: v, isDefault: true}
            else
              parameter.allowableValues.descriptiveValues.push {value: v, isDefault: false}

    # Store a named reference to this operation on the parent resource
    # getDefinitions() maps to getDefinitionsData.do()
    @resource[@nickname]= (args, callback, error) =>
      @do(args, callback, error)

    # shortcut to help method
    @resource[@nickname].help = =>
      @help()

  isListType: (type) ->
    if(type.indexOf('[') >= 0) then type.substring(type.indexOf('[') + 1, type.indexOf(']')) else undefined

  getSignature: (type, models) ->
    # set listType if it exists
    listType = @isListType(type)

    # set flag which says if its primitive or not
    isPrimitive = if ((listType? and models[listType]) or models[type]?) then false else true

    if (isPrimitive) then type else (if listType? then models[listType].getMockSignature() else models[type].getMockSignature())

  getSampleJSON: (type, models) ->
    # set listType if it exists
    listType = @isListType(type)

    # set flag which says if its primitive or not
    isPrimitive = if ((listType? and models[listType]) or models[type]?) then false else true

    val = if (isPrimitive) then undefined else (if listType? then models[listType].createJSONSample() else models[type].createJSONSample())

    # pretty printing obtained JSON
    if val
      # if container is list wrap it
      val = if listType then [val] else val
      JSON.stringify(val, null, 2)
      
  do: (args={}, opts={}, callback, error) =>
    requestContentType = null
    responseContentType = null

    # if the args is a function, then it must be a resource without
    # parameters or opts
    if (typeof args) == "function"
      error = opts
      callback = args
      args = {}

    if (typeof opts) == "function"
      error = callback
      callback = opts

    # Define a default error handler
    unless error?
      error = (xhr, textStatus, error) -> console.log xhr, textStatus, error

    # Define a default success handler
    unless callback?
      callback = (data) ->
        content = null
        if data.content?
          content = data.content.data
        else
          content = "no data"
        console.log "default callback: " + content
    
    # params to pass into the request
    params = {}

    # Pull headers out of args    
    if args.headers?
      params.headers = args.headers
      delete args.headers
      
    # Pull body out of args
    if args.body?
      params.body = args.body
      delete args.body

    # pull out any form params
    possibleParams = (param for param in @parameters when (param.paramType is "form" or param.paramType.toLowerCase() is "file" ))
    if possibleParams
      for key, value of possibleParams
        if args[value.name]
          params[value.name] = args[value.name]

    req = new SwaggerRequest(@method, @urlify(args), params, opts, callback, error, this)
    if opts.mock?
      req
    else
      true

  pathJson: -> @path.replace "{format}", "json"

  pathXml: -> @path.replace "{format}", "xml"

  # converts the operation path into a real URL, and appends query params
  urlify: (args) ->
    url = @resource.basePath + @pathJson()

    # Iterate over allowable params, interpolating the 'path' params into the url string.
    # Whatever's left over in the args object will become the query string
    for param in @parameters
      if param.paramType == 'path'
        if args[param.name]
          reg = new RegExp '\{'+param.name+'[^\}]*\}', 'gi'
          url = url.replace(reg, encodeURIComponent(args[param.name]))
          delete args[param.name]
        else
          throw "#{param.name} is a required path param."

    # Append the query string to the URL
    queryParams = ""
    for param in @parameters
      if param.paramType == 'query'
        if args[param.name]
          if queryParams != ""
            queryParams += "&"
          queryParams += encodeURIComponent(param.name) + '=' + encodeURIComponent(args[param.name])

    url += ("?" + queryParams) if queryParams? and queryParams.length > 0

    url

  # expose default headers
  supportHeaderParams: ->
    @resource.api.supportHeaderParams

  # expose supported submit methods
  supportedSubmitMethods: ->
    @resource.api.supportedSubmitMethods

  getQueryParams: (args) ->
    @getMatchingParams ['query'], args
 
  getHeaderParams: (args) ->
    @getMatchingParams ['header'], args

  # From args extract params of paramType and return them
  getMatchingParams: (paramTypes, args) ->
    matchingParams = {}
    for param in @parameters
      if args and args[param.name]
        matchingParams[param.name] = args[param.name]

    for name, value of @resource.api.headers
      matchingParams[name] = value

    matchingParams

  help: ->
    msg = ""
    for parameter in @parameters
      if msg isnt "" then msg += "\n"
      msg += "* #{parameter.name}#{if parameter.required then ' (required)'  else ''} - #{parameter.description}"
    msg


# Swagger Request turns an operation into an actual request
class SwaggerRequest
  constructor: (@type, @url, @params, @opts, @successCallback, @errorCallback, @operation, @execution) ->
    throw "SwaggerRequest type is required (get/post/put/delete)." unless @type?
    throw "SwaggerRequest url is required." unless @url?
    throw "SwaggerRequest successCallback is required." unless @successCallback?
    throw "SwaggerRequest error callback is required." unless @errorCallback?
    throw "SwaggerRequest operation is required." unless @operation?

    @type = @type.toUpperCase()
    headers = params.headers
    myHeaders = {}
    body = params.body
    parent = params["parent"]

    requestContentType = "application/json"
    # if post or put, set the content-type being sent, otherwise make it null
    # some servers will die if content-type is set but there is no body
    if body and (@type is "POST" or @type is "PUT" or @type is "PATCH")
      if @opts.requestContentType
        requestContentType = @opts.requestContentType
    else
      # if any form params, content-type must be set
      if (param for param in @operation.parameters when param.paramType is "form").length > 0
        type = param.type || param.dataType
        if (param for param in @operation.parameters when type.toLowerCase() is "file").length > 0
          requestContentType = "multipart/form-data"
        else
          requestContentType = "application/x-www-form-urlencoded"
      else if @type isnt "DELETE"
        requestContentType = null

    # verify the content type is acceptable from what it defines
    if requestContentType and @operation.consumes
      if @operation.consumes.indexOf(requestContentType) is -1
        console.log "server doesn't consume " + requestContentType + ", try " + JSON.stringify(@operation.consumes)
        if @requestContentType == null
          requestContentType = @operation.consumes[0]

    responseContentType = null
    # if get or post, set the content-type being sent, otherwise make it null
    if (@type is "POST" or @type is "GET" or @type is "PATCH")
      if @opts.responseContentType
        responseContentType = @opts.responseContentType
      else
        responseContentType = "application/json"
    else
      responseContentType = null

    # verify the content type can be produced
    if responseContentType and @operation.produces
      if @operation.produces.indexOf(responseContentType) is -1
        console.log "server can't produce " + responseContentType

    # prepare the body from params, if needed
    if requestContentType && requestContentType.indexOf("application/x-www-form-urlencoded") is 0
      # pull fields from args
      fields = {}
      possibleParams = (param for param in @operation.parameters when param.paramType is "form")

      values = {}
      for key, value of possibleParams
        if @params[value.name]
          values[value.name] = @params[value.name]
      urlEncoded = ""
      for key, value of values
        if urlEncoded != ""
          urlEncoded += "&"
        urlEncoded += encodeURIComponent(key) + '=' + encodeURIComponent(value)
      body = urlEncoded

    if requestContentType
      myHeaders["Content-Type"] = requestContentType
    if responseContentType
      myHeaders["Accept"] = responseContentType

    unless headers? and headers.mock?

      obj = 
        url: @url
        method: @type
        headers: myHeaders
        body: body
        on:
          error: (response) =>
            @errorCallback response, @opts.parent
          redirect: (response) =>
            @successCallback response, @opts.parent
          307: (response) =>
            @successCallback response, @opts.parent
          response: (response) =>
            @successCallback response, @opts.parent

      # apply authorizations
      e = {}
      if typeof window != 'undefined'
        e = window
      else
        e = exports
      e.authorizations.apply obj

      unless opts.mock?
        new SwaggerHttp().execute obj
      else
        console.log obj
        return obj

  asCurl: ->
    header_args = ("--header \"#{k}: #{v}\"" for k,v of @headers)
    "curl #{header_args.join(" ")} #{@url}"


# SwaggerHttp is a wrapper on top of Shred, which makes actual http requests
class SwaggerHttp
  Shred: null
  shred: null
  content: null

  constructor: ->    
    if typeof window != 'undefined'
      @Shred = require "./shred"
    else
      @Shred = require "shred"
    @shred = new @Shred()

    identity = (x) => x
    toString = (x) => x.toString()
      
    if typeof window != 'undefined'
      @content = require "./shred/content"
      @content.registerProcessor(
        ["application/json; charset=utf-8","application/json","json"], { parser: (identity), stringify: toString })
    else
      @Shred.registerProcessor(
        ["application/json; charset=utf-8","application/json","json"], { parser: (identity), stringify: toString })

  execute: (obj) ->
    @shred.request obj

class SwaggerAuthorizations
  authz: null

  constructor: ->
    @authz = {}

  add: (name, auth) ->
    @authz[name] = auth
    auth

  apply: (obj) ->
    for key, value of @authz
      # see if it applies
      value.apply obj

class ApiKeyAuthorization
  type: null
  name: null
  value: null

  constructor: (name, value, type) ->
    @name = name
    @value = value
    @type = type

  apply: (obj) ->
    if @type == "query"
      if obj.url.indexOf('?') > 0
        obj.url = obj.url + "&" + @name + "=" + @value
      else
        obj.url = obj.url + "?" + @name + "=" + @value
      true
    else if @type == "header"
      obj.headers[@name] = @value

@SwaggerApi = SwaggerApi
@SwaggerResource = SwaggerResource
@SwaggerOperation = SwaggerOperation
@SwaggerRequest = SwaggerRequest
@SwaggerModelProperty = SwaggerModelProperty
@ApiKeyAuthorization = ApiKeyAuthorization

@authorizations = new SwaggerAuthorizations()
