let amf = require('amf-client-js')

let resolvedModel;
let parse = (from, url, callback) => {
    filePath = url;
    let model = { model: null, error: null };

    amf.AMF.init()
        .then(() => {
            
            let parser = resolveParser(from);

            //let validationProfile;
            //switch (type) {
            //    case 'RAML 1.0': validationProfile = amf.ProfileNames.RAML; break;
            //    case 'RAML 0.8': validationProfile = amf.ProfileNames.RAML08; break;
            //    case 'OAS 2.0':
            //    case 'OAS 3.0':
            //        validationProfile = amf.ProfileNames.OAS;
            //        break;
            //}
            //const result = await amf.AMF.validate(doc, validationProfile);

            parser.parseFileAsync(url).then((amfModel) => {

                // this has all the types, securitySchemes, traits, resourceTypes, annotations, etc
                // let declares = amfModel.declares;
                // model.shapes = mapDeclaredShapes(declares);
                //mapShapesInReferences(amfModel, model);

                if (from === "raml") {
                   //resolvedModel = amf.AMF.resolveRaml10(amfModel);
                    var resolver = new amf.Resolver("RAML 1.0")
                    resolvedModel = resolver.resolve(amfModel, "editing")
                } else if (from === "raml08") {
                    //resolvedModel = amf.AMF.resolveRaml08(amfModel);
                    var resolver = new amf.Resolver("RAML 0.8")
                    resolvedModel = resolver.resolve(amfModel, "editing")
                } else if (from === "oasyaml" || from == "oasjson") {
                  //resolvedModel = amf.AMF.resolveOas20(amfModel);
                  var resolver = new amf.Resolver("OAS 2.0")
                  resolvedModel = resolver.resolve(amfModel, "editing")
                } else {
                  //resolvedModel = amf.AMF.resolveAmfGraph(amfModel);
                  var resolver = new amf.Resolver("AMF Graph") // TODO: check!!
                  resolvedModel = resolver.resolve(amfModel, "editing")
                }

                let declares = resolvedModel.declares;
                model.shapes = mapDeclaredShapes(declares);
                mapShapesInReferences(resolvedModel, model);
                model.model = mapModel(resolvedModel);
                
                return callback(model);
            }).catch((ex) => {
                model.error = {
                    message: typeof ex.toString === "function" ? ex.toString() : null,
                    stack: ex.hasOwnProperty("stack") ? ex.stack : null
                };
                return callback(model);
            });
        })
        .catch((ex) => {
            throw ex
        });
}

let amfParse = async (from, url) => {
    filePath = url;

    await amf.AMF.init();

    let parser = resolveParser(from);

    var amfModel = await parser.parseFileAsync(url);

    if (from === "raml") {
        //resolvedModel = amf.AMF.resolveRaml10(amfModel);
        var resolver = new amf.Resolver("RAML 1.0")
        resolvedModel = resolver.resolve(amfModel, "editing")
    } else if (from === "raml08") {
        //resolvedModel = amf.AMF.resolveRaml08(amfModel);
        var resolver = new amf.Resolver("RAML 0.8")
        resolvedModel = resolver.resolve(amfModel, "editing")
    } else if (from === "oasyaml" || from == "oasjson") {
        //resolvedModel = amf.AMF.resolveOas20(amfModel);
        var resolver = new amf.Resolver("OAS 2.0")
        resolvedModel = resolver.resolve(amfModel, "editing")
    } else {
        //resolvedModel = amf.AMF.resolveAmfGraph(amfModel);
        var resolver = new amf.Resolver("AMF Graph") // TODO: check!!
        resolvedModel = resolver.resolve(amfModel, "editing")
    }

    const generator = amf.Core.generator('AMF Graph', 'application/ld+json');
    const opts = amf.render.RenderOptions().withSourceMaps.withCompactUris;
    const model = await generator.generateString(resolvedModel, opts);
    return model;
}

let resolveParser = (from) => {
    let parser;
    if (from === "raml") {
        parser = amf.Core.parser("RAML 1.0", "application/yaml");
    } else if (from === "raml08") {
        parser = amf.Core.parser("RAML 0.8", "application/yaml");
    } else if (from === "oasjson") {
        parser = amf.Core.parser("OAS 2.0", "application/json");
    } else if (from === "oasyaml") {
        parser = amf.Core.parser("OAS 2.0", "application/yaml");
    } else {
        parser = amf.Core.parser("AMF Graph", "application/ld+json");
    }
    return parser;
}

let mapShapesInReferences = (baseUnit, model) => {
    for (let i = 0; i < baseUnit.references().length; i++) {
        let refUnit = baseUnit.references()[i];
        let declaresInRef = refUnit.declares;
        if(declaresInRef){
            let moreShapes = mapDeclaredShapes(declaresInRef);
            for (let j = 0; j < moreShapes.length; j++) {
                model.shapes.push(moreShapes[j]);
            }
        }
        mapShapesInReferences(refUnit, model); // recursively check for refs
    }
}

let mapDeclaredShapes = (declares) => {
    if(declares === null)
        return [];
    
    return declares.map(function(decl){
        if('inherits' in decl){ // is a shape
            return mapShape(decl);
        }
        return null;
    });
}

let mapModel = (amfModel) => {
    let encodes = amfModel.encodes;
    if(encodes == null)
        return null;
    
    let mappedModel = {
        raw: amfModel.raw,
        name: value(encodes.name),
        description: value(encodes.description),
        host: value(encodes.host),
        schemes: mapValuesArray(encodes.schemes),
        endpoints: mapEndpoints(encodes.endPoints),
        servers: mapServers(encodes.servers),
        accepts: mapValuesArray(encodes.accepts),
        contentType: mapValuesArray(encodes.contentType),
        version: value(encodes.version),
        termsOfService: value(encodes.termsOfService),
        provider: mapOrganization(encodes.provider),
        license: mapLicense(encodes.license),
        documentations: mapDocumentations(encodes.documentations),
        baseUriParameters: mapParameters(encodes.baseUriParameters),
        security: mapSecurities(encodes.security),
        idsMap: idsMap
    }
    return mappedModel;
}

let mapServers = (servers) => {
    if(!servers)
        return [];
    
    return servers.map(function(server){
        return value(server.url);
    });
}

let mapOrganization = (organization) => {
    if(!organization)
        return null;

    return {
        url: value(organization.url),
        name: value(organization.name),
        email: value(organization.email)
    }
}

let mapLicense = (license) => {
    if(!license)
        return null;

    return {
        url: value(license.url),
        name: value(license.name)
    }
}

let mapEndpoints = (endpoints) => {
    if(!endpoints)
        return [];
    
    return endpoints.map(function(endpoint) {
        return {
            name: value(endpoint.name),
            relativePath: endpoint.relativePath,
            path: value(endpoint.path),
            description: value(endpoint.description),
            operations: mapOperations(endpoint.operations),
            parameters: mapParameters(endpoint.parameters),
            security: mapSecuritySchemes(endpoint.security)
        };
    });
}

let mapOperations = (operations) => {
    if(!operations)
        return [];

    return operations.map(function(op){
        return {
            method: value(op.method),
            name: value(op.name),
            description: value(op.description),
            deprecated: value(op.deprecated),
            summary: value(op.summary),
            documentation: mapDocumentation(op.documentation),
            schemes: mapValuesArray(op.schemes),
            accepts: mapValuesArray(op.accepts),
            contentType: mapValuesArray(op.contentType),
            request: mapRequest(op.request),
            responses: mapResponses(op.responses),
            security: mapSecuritySchemes(op.security)
        };
    });
}

let mapDocumentations = (documentations) => {
    if(!documentations)
        return [];
    
    return documentations.map(mapDocumentation);
}

let mapDocumentation = (documentation) => {
    if(!documentation)
        return null;

    return {
        url: value(documentation.url),
        description: value(documentation.description),
        title: value(documentation.title)
    };
}

let mapRequest = (request) => {
    if(!request)
        return null;
    
    return {
        queryParameters: mapParameters(request.queryParameters),
        headers: mapParameters(request.headers),
        payloads: mapPayloads(request.payloads),
        queryString: mapShape(request.queryString)
    }
}

let mapResponses = (responses) => {
    if(!responses)
        return [];
    
    return responses.map(function(response){
        return {
            name: value(response.name),
            description: value(response.description),
            statusCode: value(response.statusCode),
            headers: mapParameters(response.headers),
            payloads: mapPayloads(response.payloads),
            examples: mapExamples(response.examples)
        }
    });
}

let mapSecurities = (securities) => {
    if(!securities)
        return [];
    
    return securities.map(mapSecurity);
}

let mapSecurity = (security) => {
    if(!security)
        return [];
    
    return security.map(function(sec){
        return {
            name: value(sec.name),
            scheme: mapSecurityScheme(sec.scheme),
            settings: mapSettings(sec.settings)
        }
    });
}

let mapSecuritySchemes = (securitySchemes) => {
    if(!securitySchemes)
        return [];
    
    return securitySchemes.map(mapSecurityScheme);
}

let mapSecurityScheme = (aScheme) => {
    if(!aScheme)
        return null;
    
    var scheme = aScheme.scheme;
    if(!scheme)
        return null;
    
    return {
        name: value(scheme.name),
        type: value(scheme.type),
        displayName: value(scheme.displayName),
        description: value(scheme.description),
        headers: mapParameters(scheme.headers),
        queryParameters: mapParameters(scheme.queryParameters),
        responses: mapResponses(scheme.responses),
        settings: mapSettings(scheme.settings, value(scheme.type)),
        queryString: mapShape(scheme.queryString)
    };
}

let mapSettings = (settings, type) => {
    if(!settings)
        return null;

    if(type === 'OAuth 1.0'){
        return {
            requestTokenUri: value(settings.requestTokenUri),
            authorizationUri: value(settings.authorizationUri),
            tokenCredentialsUri: value(settings.tokenCredentialsUri),
            signatures: mapValuesArray(settings.signatures)
        };
    }

    if(type === 'OAuth 2.0'){
        return {
            authorizationUri: value(settings.authorizationUri),
            accessTokenUri: value(settings.accessTokenUri),
            authorizationGrants: mapValuesArray(settings.authorizationGrants),
            flow: value(settings.flow),
            scopes: mapScopes(settings.scopes)
        };
    }

    if(type === 'ApiKey'){
        return {
            name: value(settings.name),
            in: value(settings.in)
        };
    }
    
    return {
        requestTokenUri: value(settings.requestTokenUri),
        authorizationUri: value(settings.authorizationUri),
        tokenCredentialsUri: value(settings.tokenCredentialsUri),
        signatures: mapValuesArray(settings.signatures),
        accessTokenUri: value(settings.accessTokenUri),
        authorizationGrants: mapValuesArray(settings.authorizationGrants),
        flow: value(settings.flow),
        scopes: mapScopes(settings.scopes),
        name: value(settings.name),
        in: value(settings.in)
    };
}

let mapScopes = (scopes) => {
    if(!scopes)
        return [];

    return scopes.map(function(scope){
        return {
            name: value(scope.name),
            description: value(scope.description)
        };
    });
}

let mapParameters = (parameters) => {
    if(!parameters)
        return [];
    
    return parameters.map(function(param){
        return {
            name: value(param.name),
            description: value(param.description),
            required: value(param.required),
            binding: value(param.binding),
            schema: mapSchema(param.schema)
        }
    });
}

let mapPayloads = (payloads) => {
    if(!payloads)
        return [];
    
    return payloads.map(function(payload){
        return {
            mediaType: value(payload.mediaType),
            schema: mapSchema(payload.schema)
        }
    });
}

let mapSchema = (schema) => {
    if(!schema)
        return null;

    let result = mapShape(schema);
    // result.mediaType = value(schema.mediaType);
    // result.raw = value(schema.raw);
    return result;
}

let idsMap = {};

let mapShape = (shape) => {
    if(!shape)
        return null;

    // let obj;
    // if(typeof(resolvedModel) != "undefined" && typeof(shape.annotations().resolvedLink) != "undefined")
    //     obj = resolvedModel.findById(shape.annotations().resolvedLink);
    
    // if(typeof(shape.annotations().resolvedLink) != "undefined" && shape.annotations().resolvedLink != shape.id)
    // {
    //     var key = (' ' + shape.annotations().resolvedLink).slice(1);
    //     idsMap[key] = (' ' + shape.id).slice(1);
    // }

    return {
        // shape
        id: shape.id,
        resolvedLink: typeof shape.annotations === "function" && typeof shape.annotations().resolvedLink === "string" ? shape.annotations().resolvedLink : null,
        resolvedLinkTarget: typeof shape.annotations === "function" && typeof shape.annotations().resolvedLinkTarget === "string" ? shape.annotations().resolvedLinkTarget : null,
        inheritanceProvenance: typeof shape.annotations === "function" && typeof shape.annotations().inheritanceProvenance === "string" ? shape.annotations().inheritanceProvenance : null,
        name: value(shape.name),
        displayName: value(shape.displayName),
        description: value(shape.description),
        default: mapShape(shape.defaultValue),
        values: mapEnumValues(shape.values),
        inherits: mapShapes(shape.inherits),
        isFile: shape instanceof amf.model.domain.FileShape,

        // any
        documentation: mapDocumentation(shape.documentation),
        xmlSerialization: mapXmlSerialization(shape.xmlSerialization),
        examples: mapExamples(shape.examples),

        // union
        anyOf: mapShapes(shape.anyOf),

        // array
        minItems: value(shape.minItems),
        maxItems: value(shape.maxItems),
        uniqueItems: value(shape.uniqueItems),
        items: mapShape(shape.items),
        linkTarget: mapLinkTarget(shape.linkTarget),
        linkLabel: value(shape.linkLabel),

        // scalar
        dataType: value(shape.dataType),
        pattern: value(shape.pattern),
        minLength: value(shape.minLength),
        maxLength: value(shape.maxLength),
        minimum: value(shape.minimum),
        maximum: value(shape.maximum),
        exclusiveMinimum: value(shape.exclusiveMinimum),
        exclusiveMaximum: value(shape.exclusiveMaximum),
        format: value(shape.format),
        multipleOf: value(shape.multipleOf),
        
        // node
        minProperties: value(shape.minProperties),
        maxProperties: value(shape.maxProperties),
        closed: value(shape.closed),
        discriminator: value(shape.discriminator),
        discriminatorValue: value(shape.discriminatorValue),
        readOnly: value(shape.readOnly),
        properties: mapProperties(shape.properties),
        dependencies: mapDependencies(shape.dependencies),
        
        // file
        fileTypes: mapValuesArray(shape.fileTypes)
    }
}

let value = (val) => {
    if(typeof val != 'undefined') {

        if (val === null)
            return null;

        if (typeof val.isNull != 'undefined'){
            return val.isNull ? null : val.value();
        }
        return null;
    }
    return null;
}

let mapValuesArray = (array) => {
    if(array == null)
        return [];
    
        return array.map(function(val) {
            return value(val);
        });
}

let mapEnumValues = (values) => {
    if(values == null)
        return [];
    
        if(typeof(values) == "function")
            return [];

        return values.map(function(val) {
            if(typeof val.value.option == "string")
                return val.value.option;

            if(typeof val.value.dataType == "string")
                return val.value.dataType;
            
            throw new Error("enum value of uknown type");
        });
}

let mapLinkTarget = (linkTarget) => {
    if (linkTarget == null)
        return null;

    return {
        name: linkTarget.id.substring(linkTarget.id.lastIndexOf('#') + 1)
    };
}

let mapProperties = (properties) => {
    if(!properties)
        return [];

    return properties.map(function(prop) {
        return {
            path: value(prop.path),
            range: mapShape(prop.range),
            minCount: value(prop.minCount),
            maxCount: value(prop.maxCount),
            inheritanceProvenance: typeof prop.annotations === "function"
                && typeof prop.annotations().inheritanceProvenance === "string"
                ? prop.annotations().inheritanceProvenance
                : null
        };
    });
}

let mapDependencies = (dependencies) => {
    if(!dependencies)
        return [];

    return dependencies.map(function(dep){
        return {
            propertySource: value(dep.propertySource),
            propertyTarget: value(dep.propertyTarget)
        }
    });
}

let mapShapes = (shapes) => {
    if(!shapes)
        return [];

    return shapes.map(mapShape);
}

let mapAnyShape = (shape) => {
    if(!shape)
        return null;

    return {
        documentation: mapDocumentation(shape.documentation),
        examples: mapExamples(shape.examples),
        xmlSerialization: mapXmlSerialization(shape.xmlSerialization)
    }
}

let mapExamples = (examples) => {
    if(!examples)
        return [];

    return examples.map(function(example){
        return {
            name: value(example.name),
            displayName: value(example.displayName),
            description: value(example.description),
            value: value(example.value),
            strict: value(example.strict),
            mediaType: value(example.mediaType)
        }
    });
}

let mapXmlSerialization = (xmlSerialization) => {
    if(!xmlSerialization)
        return null;
    
    return {
        attribute: value(xmlSerialization.attribute),
        wrapped: value(xmlSerialization.wrapped),
        name: value(xmlSerialization.name),
        namespace: value(xmlSerialization.namespace),
        prefix: value(xmlSerialization.prefix)
    }
}

exports.parse = parse;
exports.amfParse = amfParse;