/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed 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.
 */

package com.android.scenegraph;
import com.android.scenegraph.CompoundTransform.TranslateComponent;
import com.android.scenegraph.CompoundTransform.RotateComponent;
import com.android.scenegraph.CompoundTransform.ScaleComponent;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.HashMap;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import android.renderscript.*;
import android.util.Log;

public class ColladaParser {
    static final String TAG = "ColladaParser";
    Document mDom;

    HashMap<String, LightBase> mLights;
    HashMap<String, Camera> mCameras;
    HashMap<String, ArrayList<ShaderParam> > mEffectsParams;
    HashMap<String, Texture2D> mImages;
    HashMap<String, Texture2D> mSamplerImageMap;
    HashMap<String, String> mMeshIdNameMap;
    Scene mScene;

    String mRootDir;

    String toString(Float3 v) {
        String valueStr = v.x + " " + v.y + " " + v.z;
        return valueStr;
    }

    String toString(Float4 v) {
        String valueStr = v.x + " " + v.y + " " + v.z + " " + v.w;
        return valueStr;
    }

    public ColladaParser(){
        mLights = new HashMap<String, LightBase>();
        mCameras = new HashMap<String, Camera>();
        mEffectsParams = new HashMap<String, ArrayList<ShaderParam> >();
        mImages = new HashMap<String, Texture2D>();
        mMeshIdNameMap = new HashMap<String, String>();
    }

    public void init(InputStream is, String rootDir) {
        mLights.clear();
        mCameras.clear();
        mEffectsParams.clear();

        mRootDir = rootDir;

        long start = System.currentTimeMillis();
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        try {
            DocumentBuilder db = dbf.newDocumentBuilder();
            mDom = db.parse(is);
        } catch(ParserConfigurationException e) {
            e.printStackTrace();
        } catch(SAXException e) {
            e.printStackTrace();
        } catch(IOException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        Log.v("TIMER", "    Parse time: " + (end - start));
        exportSceneData();
    }

    Scene getScene() {
        return mScene;
    }

    private void exportSceneData(){
        mScene = new Scene();

        Element docEle = mDom.getDocumentElement();
        NodeList nl = docEle.getElementsByTagName("light");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element l = (Element)nl.item(i);
                convertLight(l);
            }
        }

        nl = docEle.getElementsByTagName("camera");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element c = (Element)nl.item(i);
                convertCamera(c);
            }
        }

        nl = docEle.getElementsByTagName("image");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element img = (Element)nl.item(i);
                convertImage(img);
            }
        }

        nl = docEle.getElementsByTagName("effect");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element e = (Element)nl.item(i);
                convertEffects(e);
            }
        }

        // Material is just a link to the effect
        nl = docEle.getElementsByTagName("material");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element m = (Element)nl.item(i);
                convertMaterials(m);
            }
        }

        // Look through the geometry list and build up a correlation between id's and names
        nl = docEle.getElementsByTagName("geometry");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element m = (Element)nl.item(i);
                convertGeometries(m);
            }
        }


        nl = docEle.getElementsByTagName("visual_scene");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element s = (Element)nl.item(i);
                getScene(s);
            }
        }
    }

    private void getRenderable(Element shape, Transform t) {
        String geoURL = shape.getAttribute("url").substring(1);
        String geoName = mMeshIdNameMap.get(geoURL);
        if (geoName != null) {
            geoURL = geoName;
        }
        //RenderableGroup group = new RenderableGroup();
        //group.setName(geoURL.substring(1));
        //mScene.appendRenderable(group);
        NodeList nl = shape.getElementsByTagName("instance_material");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element materialRef = (Element)nl.item(i);
                String meshIndexName = materialRef.getAttribute("symbol");
                String materialName = materialRef.getAttribute("target");

                Renderable d = new Renderable();
                d.setMesh(geoURL, meshIndexName);
                d.setMaterialName(materialName.substring(1));
                d.setName(geoURL);

                //Log.v(TAG, "Created drawable geo " + geoURL + " index " + meshIndexName + " material " + materialName);

                d.setTransform(t);
                //Log.v(TAG, "Set source param " + t.getName());

                // Now find all the parameters that exist on the material
                ArrayList<ShaderParam> materialParams;
                materialParams = mEffectsParams.get(materialName.substring(1));
                for (int pI = 0; pI < materialParams.size(); pI ++) {
                    d.appendSourceParams(materialParams.get(pI));
                    //Log.v(TAG, "Set source param i: " + pI + " name " + materialParams.get(pI).getParamName());
                }
                mScene.appendRenderable(d);
                //group.appendChildren(d);
            }
        }
    }

    private void updateLight(Element shape, Transform t) {
        String lightURL = shape.getAttribute("url");
        // collada uses a uri structure to link things,
        // but we ignore it for now and do a simple search
        LightBase light = mLights.get(lightURL.substring(1));
        if (light != null) {
            light.setTransform(t);
            //Log.v(TAG, "Set Light " + light.getName() + " " + t.getName());
        }
    }

    private void updateCamera(Element shape, Transform t) {
        String camURL = shape.getAttribute("url");
        // collada uses a uri structure to link things,
        // but we ignore it for now and do a simple search
        Camera cam = mCameras.get(camURL.substring(1));
        if (cam != null) {
            cam.setTransform(t);
            //Log.v(TAG, "Set Camera " + cam.getName() + " " + t.getName());
        }
    }

    private void getNode(Element node, Transform parent, String indent) {
        String name = node.getAttribute("name");
        String id = node.getAttribute("id");
        CompoundTransform current = new CompoundTransform();
        current.setName(name);
        if (parent != null) {
            parent.appendChild(current);
        } else {
            mScene.appendTransform(current);
        }

        mScene.addToTransformMap(current);

        //Log.v(TAG, indent + "|");
        //Log.v(TAG, indent + "[" + name + "]");

        Node childNode = node.getFirstChild();
        while (childNode != null) {
            if (childNode.getNodeType() == Node.ELEMENT_NODE) {
                Element field = (Element)childNode;
                String fieldName = field.getTagName();
                String description = field.getAttribute("sid");
                if (fieldName.equals("translate")) {
                    Float3 value = getFloat3(field);
                    current.addTranslate(description, value);
                    //Log.v(TAG, indent + " translate " + description + toString(value));
                } else if (fieldName.equals("rotate")) {
                    Float4 value = getFloat4(field);
                    //Log.v(TAG, indent + " rotate " + description + toString(value));
                    Float3 axis = new Float3(value.x, value.y, value.z);
                    current.addRotate(description, axis, value.w);
                } else if (fieldName.equals("scale")) {
                    Float3 value = getFloat3(field);
                    //Log.v(TAG, indent + " scale " + description + toString(value));
                    current.addScale(description, value);
                } else if (fieldName.equals("instance_geometry")) {
                    getRenderable(field, current);
                } else if (fieldName.equals("instance_light")) {
                    updateLight(field, current);
                } else if (fieldName.equals("instance_camera")) {
                    updateCamera(field, current);
                } else if (fieldName.equals("node")) {
                    getNode(field, current, indent + "   ");
                }
            }
            childNode = childNode.getNextSibling();
        }
    }

    // This will find the actual texture node, which is sometimes hidden behind a sampler
    // and sometimes referenced directly
    Texture2D getTexture(String samplerName) {
        String texName = samplerName;

        // Check to see if the image file is hidden by a sampler surface link combo
        Element sampler = mDom.getElementById(samplerName);
        if (sampler != null) {
            NodeList nl = sampler.getElementsByTagName("source");
            if (nl != null && nl.getLength() == 1) {
                Element ref = (Element)nl.item(0);
                String surfaceName = getString(ref);
                if (surfaceName == null) {
                    return null;
                }

                Element surface = mDom.getElementById(surfaceName);
                if (surface == null) {
                    return null;
                }
                nl = surface.getElementsByTagName("init_from");
                if (nl != null && nl.getLength() == 1) {
                    ref = (Element)nl.item(0);
                    texName = getString(ref);
                }
            }
        }

        //Log.v(TAG, "Extracted texture name " + texName);
        return mImages.get(texName);
    }

    void extractParams(Element fx, ArrayList<ShaderParam> params) {
        Node paramNode = fx.getFirstChild();
        while (paramNode != null) {
            if (paramNode.getNodeType() == Node.ELEMENT_NODE) {
                String name = paramNode.getNodeName();
                // Now find what type it is
                Node typeNode = paramNode.getFirstChild();
                while (typeNode != null && typeNode.getNodeType() != Node.ELEMENT_NODE) {
                    typeNode = typeNode.getNextSibling();
                }
                String paramType = typeNode.getNodeName();
                Element typeElem = (Element)typeNode;
                ShaderParam sceneParam = null;
                if (paramType.equals("color")) {
                    Float4Param f4p = new Float4Param(name);
                    Float4 value = getFloat4(typeElem);
                    f4p.setValue(value);
                    sceneParam = f4p;
                    //Log.v(TAG, "Extracted " + sceneParam.getParamName() + " value " + toString(value));
                } else if (paramType.equals("float")) {
                    Float4Param f4p = new Float4Param(name);
                    float value = getFloat(typeElem);
                    f4p.setValue(new Float4(value, value, value, value));
                    sceneParam = f4p;
                    //Log.v(TAG, "Extracted " + sceneParam.getParamName() + " value " + value);
                }  else if (paramType.equals("texture")) {
                    String samplerName = typeElem.getAttribute("texture");
                    Texture2D tex = getTexture(samplerName);
                    TextureParam texP = new TextureParam(name);
                    texP.setTexture(tex);
                    sceneParam = texP;
                    //Log.v(TAG, "Extracted texture " + tex);
                }
                if (sceneParam != null) {
                    params.add(sceneParam);
                }
            }
            paramNode = paramNode.getNextSibling();
        }
    }

    private void convertMaterials(Element mat) {
        String id = mat.getAttribute("id");
        NodeList nl = mat.getElementsByTagName("instance_effect");
        if (nl != null && nl.getLength() == 1) {
            Element ref = (Element)nl.item(0);
            String url = ref.getAttribute("url");
            ArrayList<ShaderParam> params = mEffectsParams.get(url.substring(1));
            mEffectsParams.put(id, params);
        }
    }

    private void convertGeometries(Element geo) {
        String id = geo.getAttribute("id");
        String name = geo.getAttribute("name");
        if (!id.equals(name)) {
            mMeshIdNameMap.put(id, name);
        }
    }

    private void convertEffects(Element fx) {
        String id = fx.getAttribute("id");
        ArrayList<ShaderParam> params = new ArrayList<ShaderParam>();

        NodeList nl = fx.getElementsByTagName("newparam");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element field = (Element)nl.item(i);
                field.setIdAttribute("sid", true);
            }
        }

        nl = fx.getElementsByTagName("blinn");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element field = (Element)nl.item(i);
                //Log.v(TAG, "blinn");
                extractParams(field, params);
            }
        }
        nl = fx.getElementsByTagName("lambert");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element field = (Element)nl.item(i);
                //Log.v(TAG, "lambert");
                extractParams(field, params);
            }
        }
        nl = fx.getElementsByTagName("phong");
        if (nl != null) {
            for(int i = 0; i < nl.getLength(); i++) {
                Element field = (Element)nl.item(i);
                //Log.v(TAG, "phong");
                extractParams(field, params);
            }
        }
        mEffectsParams.put(id, params);
    }

    private void convertLight(Element light) {
        String name = light.getAttribute("name");
        String id = light.getAttribute("id");

        // Determine type
        String[] knownTypes = { "point", "spot", "directional" };
        final int POINT_LIGHT = 0;
        final int SPOT_LIGHT = 1;
        final int DIR_LIGHT = 2;
        int type = -1;
        for (int i = 0; i < knownTypes.length; i ++) {
            NodeList nl = light.getElementsByTagName(knownTypes[i]);
            if (nl != null && nl.getLength() != 0) {
                type = i;
                break;
            }
        }

        //Log.v(TAG, "Found Light Type " + type);

        LightBase sceneLight = null;
        switch (type) {
        case POINT_LIGHT:
            sceneLight = new PointLight();
            break;
        case SPOT_LIGHT: // TODO: finish light types
            break;
        case DIR_LIGHT: // TODO: finish light types
            break;
        }

        if (sceneLight == null) {
            return;
        }

        Float3 color = getFloat3(light, "color");
        sceneLight.setColor(color.x, color.y, color.z);
        sceneLight.setName(name);
        mScene.appendLight(sceneLight);
        mLights.put(id, sceneLight);

        //Log.v(TAG, "Light " + name + " color " + toString(color));
    }

    private void convertCamera(Element camera) {
        String name = camera.getAttribute("name");
        String id = camera.getAttribute("id");
        float fov = 30.0f;
        if (getString(camera, "yfov") != null) {
            fov = getFloat(camera, "yfov");
        } else if(getString(camera, "xfov") != null) {
            float aspect = getFloat(camera, "aspect_ratio");
            fov = getFloat(camera, "xfov") / aspect;
        }

        float near = getFloat(camera, "znear");
        float far = getFloat(camera, "zfar");

        Camera sceneCamera = new Camera();
        sceneCamera.setFOV(fov);
        sceneCamera.setNear(near);
        sceneCamera.setFar(far);
        sceneCamera.setName(name);
        mScene.appendCamera(sceneCamera);
        mCameras.put(id, sceneCamera);
    }

    private void convertImage(Element img) {
        String name = img.getAttribute("name");
        String id = img.getAttribute("id");
        String file = getString(img, "init_from");

        Texture2D tex = new Texture2D();
        tex.setFileName(file);
        tex.setFileDir(mRootDir);
        mScene.appendTextures(tex);
        mImages.put(id, tex);
    }

    private void getScene(Element scene) {
        String name = scene.getAttribute("name");
        String id = scene.getAttribute("id");

        Node childNode = scene.getFirstChild();
        while (childNode != null) {
            if (childNode.getNodeType() == Node.ELEMENT_NODE) {
                String indent = "";
                getNode((Element)childNode, null, indent);
            }
            childNode = childNode.getNextSibling();
        }
    }

    private String getString(Element elem, String name) {
        String text = null;
        NodeList nl = elem.getElementsByTagName(name);
        if (nl != null && nl.getLength() != 0) {
            text = ((Element)nl.item(0)).getFirstChild().getNodeValue();
        }
        return text;
    }

    private String getString(Element elem) {
        String text = null;
        text = elem.getFirstChild().getNodeValue();
        return text;
    }

    private int getInt(Element elem, String name) {
        return Integer.parseInt(getString(elem, name));
    }

    private float getFloat(Element elem, String name) {
        return Float.parseFloat(getString(elem, name));
    }

    private float getFloat(Element elem) {
        return Float.parseFloat(getString(elem));
    }

    private Float3 parseFloat3(String valueString) {
        StringTokenizer st = new StringTokenizer(valueString);
        float x = Float.parseFloat(st.nextToken());
        float y = Float.parseFloat(st.nextToken());
        float z = Float.parseFloat(st.nextToken());
        return new Float3(x, y, z);
    }

    private Float4 parseFloat4(String valueString) {
        StringTokenizer st = new StringTokenizer(valueString);
        float x = Float.parseFloat(st.nextToken());
        float y = Float.parseFloat(st.nextToken());
        float z = Float.parseFloat(st.nextToken());
        float w = Float.parseFloat(st.nextToken());
        return new Float4(x, y, z, w);
    }

    private Float3 getFloat3(Element elem, String name) {
        String valueString = getString(elem, name);
        return parseFloat3(valueString);
    }

    private Float4 getFloat4(Element elem, String name) {
        String valueString = getString(elem, name);
        return parseFloat4(valueString);
    }

    private Float3 getFloat3(Element elem) {
        String valueString = getString(elem);
        return parseFloat3(valueString);
    }

    private Float4 getFloat4(Element elem) {
        String valueString = getString(elem);
        return parseFloat4(valueString);
    }
}
