# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

import xml.dom.minidom
import json
from optparse import OptionParser
from textwrap import dedent
import os
import sys
import urllib2


class cmdParameterProperty(object):

    def __init__(self):
        self.name = None
        self.required = False
        self.desc = ""
        self.type = "planObject"
        self.subProperties = []
        self.dataType = ""


class cloudStackCmd(object):

    def __init__(self):
        self.name = ""
        self.desc = ""
        self.async = "false"
        self.request = []
        self.response = []


class CodeGenerator(object):

    """
    Apache CloudStack- marvin python classes can be generated from the json
    returned by API discovery or from the xml spec of commands generated by
    the ApiDocWriter. This class provides helper methods for these uses.
    """
    space = '    '
    newline = '\n'
    cmdsName = []

    def __init__(self, outputFolder):
        self.cmd = None
        self.code = ""
        self.required = []
        self.subclass = []
        self.outputFolder = outputFolder
        lic = """\
          # Licensed to the Apache Software Foundation (ASF) under one
          # or more contributor license agreements.  See the NOTICE file
          # distributed with this work for additional information
          # regarding copyright ownership.  The ASF licenses this file
          # to you under the Apache License, Version 2.0 (the
          # "License"); you may not use this file except in compliance
          # with the License.  You may obtain a copy of the License at
          #
          #   http://www.apache.org/licenses/LICENSE-2.0
          #
          # Unless required by applicable law or agreed to in writing,
          # software distributed under the License is distributed on an
          # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
          # KIND, either express or implied.  See the License for the
          # specific language governing permissions and limitations
          # under the License.

          """
        self.license = dedent(lic)

    def addAttribute(self, attr, pro):
        value = pro.value
        if pro.required:
            self.required.append(attr)
        desc = pro.desc
        if desc is not None:
            self.code += self.space
            self.code += "''' " + pro.desc + " '''"
            self.code += self.newline

        self.code += self.space
        self.code += attr + " = " + str(value)
        self.code += self.newline

    def generateSubClass(self, name, properties):
        '''generate code for sub list'''
        subclass = 'class %s:\n' % name
        subclass += self.space + "def __init__(self):\n"
        for pro in properties:
            if pro.desc is not None:
                subclass += self.space + self.space + '""""%s"""\n' % pro.desc
            if len(pro.subProperties) > 0:
                subclass += self.space + self.space
                subclass += 'self.%s = []\n' % pro.name
                self.generateSubClass(pro.name, pro.subProperties)
            else:
                subclass += self.space + self.space
                subclass += 'self.%s = None\n' % pro.name

        self.subclass.append(subclass)

    def generate(self, cmd):

        self.cmd = cmd
        self.cmdsName.append(self.cmd.name)
        self.code = self.license
        self.code += self.newline
        self.code += '"""%s"""\n' % self.cmd.desc
        self.code += 'from baseCmd import *\n'
        self.code += 'from baseResponse import *\n'
        self.code += "class %sCmd (baseCmd):\n" % self.cmd.name
        self.code += self.space
        self.code += 'typeInfo = {}\n'
        self.code += self.space + "def __init__(self):\n"

        self.code += self.space + self.space
        self.code += 'self.isAsync = "%s"\n' % str(self.cmd.async).lower()

        for req in self.cmd.request:
            if req.desc is not None:
                self.code += self.space + self.space + '"""%s"""\n' % req.desc
            if req.required == "true":
                self.code += self.space + self.space + '"""Required"""\n'

            value = "None"
            if req.type == "list" or req.type == "map":
                value = "[]"

            self.code += self.space + self.space
            self.code += 'self.%s = %s\n' % (req.name, value)
            if req.required == "true":
                self.required.append(req.name)
            self.code += self.space + self.space
            self.code += "self.typeInfo['%s'] = '%s'\n" % ( req.name, req.dataType )

        self.code += self.space + self.space + "self.required = ["
        for require in self.required:
            self.code += '"' + require + '",'
        self.code += "]\n"
        self.required = []

        """generate response code"""
        subItems = {}
        self.code += self.newline
        self.code += 'class %sResponse (baseResponse):\n' % self.cmd.name
        self.code += self.space
        self.code += 'typeInfo = {}\n'
        self.code += self.space + "def __init__(self):\n"
        if len(self.cmd.response) == 0:
            self.code += self.space + self.space + "pass"
        else:
            for res in self.cmd.response:
                if res.desc is not None:
                    self.code += self.space + self.space
                    self.code += '"""%s"""\n' % res.desc

                if len(res.subProperties) > 0:
                    self.code += self.space + self.space
                    self.code += 'self.%s = []\n' % res.name
                    self.generateSubClass(res.name, res.subProperties)
                else:
                    self.code += self.space + self.space
                    self.code += 'self.%s = None\n' % res.name
                    if res.dataType is not None:
                        self.code += self.space + self.space
                        self.code += "self.typeInfo['%s'] = '%s'\n" % ( res.name, res.dataType )

        self.code += self.newline

        for subclass in self.subclass:
            self.code += subclass + "\n"

        fp = open(self.outputFolder + "/cloudstackAPI/%s.py" % self.cmd.name,
                  "w")
        fp.write(self.code)
        fp.close()
        self.code = ""
        self.subclass = []

    def finalize(self):
        '''generate an api call'''

        header = '"""Test Client for CloudStack API"""\n'
        imports = "import copy\n"
        initCmdsList = '__all__ = ['
        body = ''
        body += "class CloudStackAPIClient(object):\n"
        body += self.space + 'def __init__(self, connection):\n'
        body += self.space + self.space + 'self.connection = connection\n'
        body += self.space + self.space + 'self._id = None\n'
        body += self.newline

        body += self.space + 'def __copy__(self):\n'
        body += self.space + self.space
        body += 'return CloudStackAPIClient(copy.copy(self.connection))\n'
        body += self.newline

        # The `id` property will be used to link the test with the cloud
        # resource being created
        #            @property
        #            def id(self):
        #                return self._id
        #
        #            @id.setter
        #            def id(self, identifier):
        #                self._id = identifier

        body += self.space + '@property' + self.newline
        body += self.space + 'def id(self):' + self.newline
        body += self.space * 2 + 'return self._id' + self.newline
        body += self.newline

        body += self.space + '@id.setter' + self.newline
        body += self.space + 'def id(self, identifier):' + self.newline
        body += self.space * 2 + 'self._id = identifier' + self.newline
        body += self.newline

        for cmdName in self.cmdsName:
            body += self.space
            if cmdName in ["login", "logout"]:
                body += 'def %s(self, command, method="POST"):\n' % cmdName
            else:
                body += 'def %s(self, command, method="GET"):\n' % cmdName
            body += self.space + self.space
            body += 'response = %sResponse()\n' % cmdName
            body += self.space + self.space
            body += 'response = self.connection.marvinRequest(command,'
            body += ' response_type=response, method=method)\n'
            body += self.space + self.space + 'return response\n'
            body += self.newline

            imports += 'from %s import %sResponse\n' % (cmdName, cmdName)
            initCmdsList += '"%s",' % cmdName

        fp = open(self.outputFolder + '/cloudstackAPI/cloudstackAPIClient.py',
                  'w')
        fp.write(self.license)
        for item in [header, imports, body]:
            fp.write(item)
        fp.close()

        '''generate __init__.py'''
        initCmdsList = self.license + initCmdsList + '"cloudstackAPIClient"]'
        fp = open(self.outputFolder + '/cloudstackAPI/__init__.py', 'w')
        fp.write(initCmdsList)
        fp.close()

        fp = open(self.outputFolder + '/cloudstackAPI/baseCmd.py', 'w')
        basecmd = self.license
        basecmd += '"""Base Command"""\n'
        basecmd += 'class baseCmd(object):\n'
        basecmd += self.space + 'pass\n'
        fp.write(basecmd)
        fp.close()

        fp = open(self.outputFolder + '/cloudstackAPI/baseResponse.py', 'w')
        basecmd = self.license
        basecmd += '"""Base class for response"""\n'
        basecmd += 'class baseResponse(object):\n'
        basecmd += self.space + 'pass\n'
        fp.write(basecmd)
        fp.close()

    def constructResponseFromXML(self, response):
        paramProperty = cmdParameterProperty()
        paramProperty.name = getText(response.getElementsByTagName('name'))
        paramProperty.desc = getText(response.
                                     getElementsByTagName('description'))
        dataType = response.getElementsByTagName('dataType')
        if dataType:
            paramProperty.dataType = getText(dataType)
        if paramProperty.name.find('(*)') != -1:
            '''This is a list'''
            paramProperty.name = paramProperty.name.split('(*)')[0]
            argList = response.getElementsByTagName('arguments')[0].\
                getElementsByTagName('arg')
            for subresponse in argList:
                subProperty = self.constructResponseFromXML(subresponse)
                paramProperty.subProperties.append(subProperty)
        return paramProperty

    def loadCmdFromXML(self, dom):
        cmds = []
        for cmd in dom.getElementsByTagName("command"):
            csCmd = cloudStackCmd()
            csCmd.name = getText(cmd.getElementsByTagName('name'))
            assert csCmd.name

            desc = getText(cmd.getElementsByTagName('description'))
            if desc:
                csCmd.desc = desc

            async = getText(cmd.getElementsByTagName('isAsync'))
            if async:
                csCmd.async = async

            argList = cmd.getElementsByTagName("request")[0].\
                getElementsByTagName("arg")
            for param in argList:
                paramProperty = cmdParameterProperty()

                paramProperty.name =\
                    getText(param.getElementsByTagName('name'))
                assert paramProperty.name

                required = param.getElementsByTagName('required')
                if required:
                    paramProperty.required = getText(required)

                requestDescription = param.getElementsByTagName('description')
                if requestDescription:
                    paramProperty.desc = getText(requestDescription)

                type = param.getElementsByTagName("type")
                if type:
                    paramProperty.type = getText(type)

                dataType = param.getElementsByTagName('dataType')
                if dataType:
                    paramProperty.dataType = getText(dataType)

                csCmd.request.append(paramProperty)

            responseEle = cmd.getElementsByTagName("response")[0]
            for response in responseEle.getElementsByTagName("arg"):
                if response.parentNode != responseEle:
                    continue

                paramProperty = self.constructResponseFromXML(response)
                csCmd.response.append(paramProperty)

            cmds.append(csCmd)
        return cmds

    def generateCodeFromXML(self, apiSpecFile):
        dom = xml.dom.minidom.parse(apiSpecFile)
        cmds = self.loadCmdFromXML(dom)
        for cmd in cmds:
            self.generate(cmd)
        self.finalize()

    def constructResponseFromJSON(self, response):
        paramProperty = cmdParameterProperty()
        if 'name' in response:
            paramProperty.name = response['name']
        assert paramProperty.name, "%s has no property name" % response

        if 'description' in response:
            paramProperty.desc = response['description']
        if 'type' in response:
            if response['type'] in ['list', 'map', 'set']:
                # Here list becomes a subproperty
                if 'response' in response:
                    for innerResponse in response['response']:
                        subProperty =\
                            self.constructResponseFromJSON(innerResponse)
                        paramProperty.subProperties.append(subProperty)
            paramProperty.type = response['type']
            paramProperty.dataType = response['type']
        return paramProperty

    def loadCmdFromJSON(self, apiStream):
        if apiStream is None:
            raise Exception("No APIs found through discovery")

        jsonOut = apiStream.readlines()
        assert len(jsonOut) > 0
        apiDict = json.loads(jsonOut[0])
        if 'listapisresponse' not in apiDict:
            raise Exception("API discovery plugin response failed")
        if 'count' not in apiDict['listapisresponse']:
            raise Exception("Malformed api response")

        apilist = apiDict['listapisresponse']['api']
        cmds = []
        for cmd in apilist:
            csCmd = cloudStackCmd()
            if 'name' in cmd:
                csCmd.name = cmd['name']
            assert csCmd.name

            if 'description' in cmd:
                csCmd.desc = cmd['description']

            if 'isasync' in cmd:
                csCmd.async = cmd['isasync']

            for param in cmd['params']:
                paramProperty = cmdParameterProperty()

                if 'name' in param:
                    paramProperty.name = param['name']
                assert paramProperty.name

                if 'required' in param:
                    paramProperty.required = str(param['required']).lower()

                if 'description' in param:
                    paramProperty.desc = param['description']

                if 'type' in param:
                    paramProperty.type = param['type']
                    paramProperty.dataType = param['type']

                csCmd.request.append(paramProperty)

            for response in cmd['response']:
                # FIXME: ExtractImage related APIs return empty dicts in
                # response
                if len(response) > 0:
                    paramProperty = self.constructResponseFromJSON(response)
                    csCmd.response.append(paramProperty)

            cmds.append(csCmd)
        return cmds

    def generateCodeFromJSON(self, endpointUrl):
        """
        Api Discovery plugin returns the supported APIs of a CloudStack
        endpoint.
        @return: The classes in cloudstackAPI/ formed from api discovery json
        """
        if endpointUrl.find('response=json') >= 0:
            apiStream = urllib2.urlopen(endpointUrl)
            cmds = self.loadCmdFromJSON(apiStream)
            for cmd in cmds:
                self.generate(cmd)
            self.finalize()


def getText(elements):
    return elements[0].childNodes[0].nodeValue.strip()

if __name__ == "__main__":
    parser = OptionParser()
    parser.add_option("-o", "--output", dest="output",
                      help="The path to the generated code entities, default\
 is .")
    parser.add_option("-s", "--specfile", dest="spec",
                      help="The path and name of the api spec xml file,\
 default is /etc/cloud/cli/commands.xml")
    parser.add_option("-e", "--endpoint", dest="endpoint",
                      help="The endpoint mgmt server (with open 8096) where\
 apis are discovered, default is localhost")

    (options, args) = parser.parse_args()

    folder = "."
    if options.output is not None:
        folder = options.output
    apiModule = folder + "/cloudstackAPI"
    if not os.path.exists(apiModule):
        try:
            os.mkdir(apiModule)
        except:
            print "Failed to create folder %s, due to %s" % (apiModule,
                                                             sys.exc_info())
            print parser.print_help()
            exit(2)

    apiSpecFile = "/etc/cloud/cli/commands.xml"
    if options.spec is not None:
        apiSpecFile = options.spec
        if not os.path.exists(apiSpecFile):
            print "the spec file %s does not exists" % apiSpecFile
            print parser.print_help()
            exit(1)

    cg = CodeGenerator(folder)
    if options.spec is not None:
        cg.generateCodeFromXML(apiSpecFile)
    elif options.endpoint is not None:
        endpointUrl = 'http://%s:8096/client/api?command=listApis&\
response=json' % options.endpoint
        cg.generateCodeFromJSON(endpointUrl)
