<template>
<div class="openapi">
  <md-layout md-column>
    <md-layout md-row md-flex="90" md-align="center">
      <md-layout md-column md-flex="65">
        <h2 class="md-display-2">{{api.info.title}}</h2>
        <div v-if="api.info.description" v-html="marked(api.info.description || '')"></div>
      </md-layout>
      <md-layout md-flex="5"></md-layout>
      <md-layout md-column md-flex="20">
        <md-layout md-flex="true"></md-layout>
        <md-card v-if="api.info">
          <md-list>
            <md-list-item v-if="api.info.contact && api.info.contact.url">
              <md-icon>home</md-icon> <span><a :href="api.info.contact.url">{{api.info.contact.name || api.info.contact.url}}</a></span>
            </md-list-item>
            <md-list-item v-if="api.info.contact && api.info.contact.email">
              <md-icon>email</md-icon> <span><a :href="'mailto:'+api.info.contact.email">{{api.info.contact.email}}</a></span>
            </md-list-item>
            <md-list-item v-if="api.info.version">
              <md-icon>label</md-icon> <span>{{api.info.version}}</span>
            </md-list-item>
            <md-list-item v-if="api.info.termsOfService">
              <md-icon>description</md-icon> <span><a :href="api.info.termsOfService">Terms of Service</a></span>
            </md-list-item>
          </md-list>
        </md-card>
        <md-layout md-flex="true"></md-layout>
      </md-layout>
    </md-layout>

    <md-layout md-row style="flex-wrap: nowrap;">
      <md-list class="md-dense" ref="menu">
        <md-list-item v-for="(entries, tag) in tags" :key="tag" md-expand-multiple>
          <span class="md-title">{{tag}}</span>
          <md-list-expand>
            <md-list>
              <md-list-item v-for="(entry, i) in entries" :key="i" @click.native="select(entry)" style="cursor:pointer">
                <md-subheader class="md-title" :class="{'md-accent':selectedEntry === entry}" v-html="entry.path.replace(/\//g,'<b>/</b>')"></md-subheader>
                <md-subheader :md-theme="entry.method" class="md-primary">{{entry.method}}</md-subheader>
              </md-list-item>
            </md-list>
          </md-list-expand>
        </md-list-item>
      </md-list>

      <md-layout md-flex-offset="5" md-flex="true" v-if="!selectedEntry">
        <p>Select an entry on the left to see detailed information...</p>
      </md-layout>

      <md-layout md-column md-flex-offset="5" md-flex="true" v-if="selectedEntry">
        <h2 class="md-title">{{selectedEntry.title || selectedEntry.summary}}</h2>
        <p class="entry-description" v-if="selectedEntry.description" v-html="marked(selectedEntry.description || '')"></p>
        <h3 class="md-subheading">{{selectedEntry.method.toUpperCase()}} {{ (api.servers && api.servers.length ? api.servers[0].url : '') + selectedEntry.path}}</h3>
        <md-tabs md-right class="md-transparent" style="margin-top:-54px">
          <md-tab md-label="Documentation">
            <h4 v-if="(selectedEntry.parameters && selectedEntry.parameters.length) || selectedEntry.requestBody">Parameters</h4>
            <parameters-table :selectedEntry="selectedEntry" :openSchemaDialog="openSchemaDialog" :openExamplesDialog="openExamplesDialog" />
            <h4>Responses</h4>
            <responses-table :selectedEntry="selectedEntry" :openSchemaDialog="openSchemaDialog" :openExamplesDialog="openExamplesDialog" :openFieldsDialog="openFieldsDialog" />
          </md-tab>
          <md-tab v-if="api.servers && api.servers.length" md-label="Make request">
            <md-layout md-row>
              <md-layout md-column md-flex="40">
                <h2>Request</h2>
                <request-form :selectedEntry="selectedEntry" :currentRequest="currentRequest"></request-form>
                <div>
                  <md-button class="md-raised md-accent" @click.native="request">Execute</md-button>
                </div>
              </md-layout>

              <md-layout md-column md-flex="60">
                <h2>Response</h2>
                <response-display v-if="currentResponse" :entry="selectedEntry" :response="currentResponse"></response-display>
              </md-layout>
            </md-layout>
          </md-tab>
        </md-tabs>
      </md-layout>
    </md-layout>
  </md-layout>

  <md-dialog ref="schemaDialog" class="schema-dialog">
    <md-dialog-title>Schema</md-dialog-title>

    <md-dialog-content>
      <md-tabs>
        <md-tab id="tree" md-label="Tree">
          <schema-view :schema="currentSchema"></schema-view>
        </md-tab>
        <md-tab id="raw" md-label="Raw">
          <pre>{{ stringify(currentSchema, null, 2)}}</pre>
        </md-tab>
      </md-tabs>
    </md-dialog-content>

    <md-dialog-actions>
      <md-button @click.native="$refs.schemaDialog.close()">ok</md-button>
    </md-dialog-actions>
  </md-dialog>

  <md-dialog ref="examplesDialog" class="examples-dialog">
    <md-dialog-title>Examples</md-dialog-title>

    <md-dialog-content>
      <md-tabs>
        <md-tab v-for="(example, label) in currentExamples" :md-label="label">
          <h5>{{example.summary}}</h5>
          <pre>{{ stringify(example.value, null, 2)}}</pre>
        </md-tab>
      </md-tabs>
    </md-dialog-content>

    <md-dialog-actions>
      <md-button @click.native="$refs.examplesDialog.close()">ok</md-button>
    </md-dialog-actions>
  </md-dialog>

  <md-dialog ref="fieldsDialog" class="fields-dialog">
    <md-dialog-title>Fields</md-dialog-title>

    <md-dialog-content>
       <md-table>
         <md-table-header>
           <md-table-row>
             <md-table-head>Name</md-table-head>
             <md-table-head>Description</md-table-head>
             <md-table-head>Type</md-table-head>
             <md-table-head>Values</md-table-head>
           </md-table-row>
         </md-table-header>

         <md-table-body>
           <md-table-row v-for="(field, name) in currentFields" :key="name">
             <md-table-cell>{{name}}</md-table-cell>
             <md-table-cell v-html="marked(field.description || '')"></md-table-cell>
             <md-table-cell v-if="field.schema.type !== 'array'">{{field.schema.type}}</md-table-cell>
             <md-table-cell v-if="field.schema.type === 'array'">{{field.schema.items.type}} array</md-table-cell>
             <md-table-cell v-if="field.schema.type !== 'array' && field.schema.enum">{{field.schema.enum.join(', ')}}</md-table-cell>
             <md-table-cell v-if="field.schema.type === 'array'">
               <div style="overflow-y:scroll;max-height:200px;">{{(field.schema.items.enum || []).join(', ')}}</div>
             </md-table-cell>
             <md-table-cell v-else />
           </md-table-row>
         </md-table-body>
       </md-table>
    </md-dialog-content>

    <md-dialog-actions>
      <md-button @click.native="$refs.fieldsDialog.close()">ok</md-button>
    </md-dialog-actions>
  </md-dialog>

</div>
</template>

<style lang="css">
.openapi {
   position:relative;
   overflow-x:hidden;
   height:100%;
}

.openapi #request-form {
  padding: 16px;
}

.openapi .md-table .md-table-cell.md-has-action .md-table-cell-container {
  display: inherit;
}

.schema-dialog .md-dialog, .examples-dialog .md-dialog{
  min-width: 800px;
}

.openapi .entry-description {
  margin: 0;
}
</style>

<script>
import Vue from 'vue'
import marked from 'marked'
import RequestForm from './RequestForm.vue'
import ResponseDisplay from './ResponseDisplay.vue'
import ResponsesTable from './ResponsesTable.vue'
import ParametersTable from './ParametersTable.vue'
import SchemaView from './SchemaView.vue'
import VueMaterial from 'vue-material'
import deref from 'json-schema-ref-parser'
import stringify from 'json-stringify-pretty-compact'

Vue.use(VueMaterial)

export default {
  name: 'open-api',
  components: {
    RequestForm,
    ResponseDisplay,
    ResponsesTable,
    ParametersTable,
    SchemaView
  },
  props: ['api', 'headers', 'queryParams'],
  data: () => ({
    selectedEntry: null,
    currentSchema: ' ',
    currentExamples: {},
    currentFields: {},
    currentRequest: {
      contentType: '',
      body: '',
      params: {}
    },
    currentResponse: null,
    tags: {}
  }),
  mounted: function() {
    if (this.$refs.menu.$children.length) this.$refs.menu.$children[0].toggleExpandList()
  },
  created() {
    getTags(this.api).then(tags => {
      this.tags = tags
    })
    Vue.material.registerTheme({
      get: {
        primary: 'blue'
      },
      post: {
        primary: 'green'
      },
      put: {
        primary: 'orange'
      },
      patch: {
        primary: 'orange'
      },
      delete: {
        primary: 'red'
      }
    })
  },
  methods: {
    marked,
    stringify,
    reset(entry) {
      const newParams = {};
      (entry.parameters || []).forEach(p => {
        newParams[p.name] = (p.in === 'query' && this.queryParams && this.queryParams[p.name]) || (p.in === 'header' && this.headers && this.headers[p.name]) || null
        if (!newParams[p.name]) {
          if (p.schema && p.schema.enum) {
            newParams[p.name] = p.schema.enum[0]
          }
          if (p.schema && p.schema.type === 'array') {
            newParams[p.name] = []
          }
          if (p.example) {
            newParams[p.name] = p.example
          }
        }
      })
      this.currentRequest.params = newParams
      if (entry.requestBody) {
        this.currentRequest.contentType = entry.requestBody.selectedType
        const example = entry.requestBody.content[this.currentRequest.contentType].example
        this.currentRequest.body = typeof example === 'string' ? example : stringify(example, null, 2)
      }
    },
    select(entry) {
      this.reset(entry)
      this.selectedEntry = entry
    },
    openSchemaDialog(schema) {
      this.currentSchema = schema
      this.$refs.schemaDialog.open()
    },
    openExamplesDialog(examples) {
      this.currentExamples = examples
      this.$refs.examplesDialog.open()
    },
    openFieldsDialog(fields) {
      this.currentFields = fields
      this.$refs.fieldsDialog.open()
    },
    request() {
      this.currentResponse = null
      fetch(this.currentRequest, this.selectedEntry, this.api).then(res => {
        this.currentResponse = res
      }, res => {
        this.currentResponse = res
      })
    }
  }
}

/*
 * HTTP requests utils
 */

function fetch(request, entry, api) {
  let params = Object.assign({}, ...(entry.parameters || [])
    .filter(p => p.in === 'query' && (p.schema.type === 'array' ? request.params[p.name].length : request.params[p.name]))
    .map(p => ({
      // TODO : join character for array should depend of p.style
      [p.name]: p.schema.type === 'array' ? request.params[p.name].join(',') : request.params[p.name]
    }))
  )
  let headers = Object.assign({}, ...(entry.parameters || [])
    .filter(p => p.in === 'header' && (p.schema.type === 'array' ? request.params[p.name].length : request.params[p.name]))
    .map(p => ({
      // TODO : join character for array should depend of p.style
      [p.name]: p.schema.type === 'array' ? request.params[p.name].join(',') : request.params[p.name]
    }))
  )
  const httpRequest = {
    method: entry.method,
    url: api.servers.length && (api.servers[0].url + entry.path.replace(/{(\w*)}/g, (m, key) => {
      return request.params[key]
    })),
    params,
    headers
  }
  if (entry.requestBody) {
    httpRequest.headers['Content-type'] = entry.requestBody.selectedType
    httpRequest.body = request.body
  }
  return Vue.http(httpRequest)
}

/*
 * Tags management utils
 */

const defaultStyle = {
  query: 'form',
  path: 'simple',
  header: 'simple',
  cookie: 'form'
}

function processContent(contentType, api) {
  // Spec allow examples as an item or an array. In the API or in the schema
  // we always fall back on an array
  if (contentType.schema) {
    contentType.examples = contentType.examples || contentType.schema.examples
    contentType.example = contentType.example || contentType.schema.example
  }

  if (contentType.example) {
    contentType.examples = [contentType.example]
  }
}

async function getTags(api) {
  const derefAPI = await deref.dereference(api)
  const tags = {}
  Object.keys(derefAPI.paths).forEach(path => {
    Object.keys(derefAPI.paths[path])
    .filter(method => ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].indexOf(method.toLowerCase()) !== -1)
    .forEach(method => {
      const entry = derefAPI.paths[path][method]
      entry.method = method
      entry.path = path
      // Filling tags entries
      entry.tags = entry.tags || []
      if (!entry.tags.length) {
        entry.tags.push('No category')
      }
      entry.tags.forEach(tag => {
        tags[tag] = tags[tag] || []
        tags[tag].push(entry)
      })

      entry.parameters = entry.parameters || []
      if (derefAPI.paths[path].parameters) {
        entry.parameters = derefAPI.paths[path].parameters.concat(entry.parameters)
      }
      if (entry.parameters) {
        entry.parameters.forEach(p => {
          p.style = p.style || defaultStyle[p.in]
          p.explode = p.explode || (p.style === 'form')
          p.schema = p.schema || { type: 'string' }
        })
      }
      if (entry.requestBody) {
        if (entry.requestBody.content) {
          Vue.set(entry.requestBody, 'selectedType', Object.keys(entry.requestBody.content)[0])
          entry.requestBody.required = true
          Object.values(entry.requestBody.content).forEach(contentType => processContent(contentType, api))
        }
      }

      // Some preprocessing with responses
      entry.responses = entry.responses || {}
      Object.values(entry.responses).forEach(response => {
        if (response.content) {
          // preselecting responses mime-type
          Vue.set(response, 'selectedType', Object.keys(response.content)[0])
          Object.values(response.content).forEach(contentType => processContent(contentType, api))
        }
      })
    })
  })
  return tags
}

</script>
