/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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.ide.eclipse.adt.internal.sdk;

import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.sdk.LayoutDevice.DeviceConfig;
import com.android.prefs.AndroidLocation;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.SdkConstants;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Validator;

/**
 * Manages the layout devices.
 * They can come from 3 sources: built-in, add-ons, user.
 */
public class LayoutDeviceManager {

    /**
     * A SAX error handler that captures the errors and warnings.
     * This allows us to capture *all* errors and just not get an exception on the first one.
     */
    private static class CaptureErrorHandler implements ErrorHandler {

        private final String mSourceLocation;

        private boolean mFoundError = false;

        CaptureErrorHandler(String sourceLocation) {
            mSourceLocation = sourceLocation;
        }

        public boolean foundError() {
            return mFoundError;
        }

        /**
         * @throws SAXException
         */
        public void error(SAXParseException ex) throws SAXException {
            mFoundError = true;
            AdtPlugin.log(ex, "Error validating %1$s", mSourceLocation);
        }

        /**
         * @throws SAXException
         */
        public void fatalError(SAXParseException ex) throws SAXException {
            mFoundError = true;
            AdtPlugin.log(ex, "Error validating %1$s", mSourceLocation);
        }

        /**
         * @throws SAXException
         */
        public void warning(SAXParseException ex) throws SAXException {
            // ignore those for now.
        }
    }

    private final SAXParserFactory mParserFactory;

    private List<LayoutDevice> mDefaultLayoutDevices =
        new ArrayList<LayoutDevice>();
    private List<LayoutDevice> mAddOnLayoutDevices =
        new ArrayList<LayoutDevice>();
    private final List<LayoutDevice> mUserLayoutDevices =
        new ArrayList<LayoutDevice>();
    private List<LayoutDevice> mLayoutDevices;

    LayoutDeviceManager() {
        mParserFactory = SAXParserFactory.newInstance();
        mParserFactory.setNamespaceAware(true);
    }

    public List<LayoutDevice> getCombinedList() {
        return mLayoutDevices;
    }

    public List<LayoutDevice> getDefaultLayoutDevices() {
        return mDefaultLayoutDevices;
    }

    public List<LayoutDevice> getAddOnLayoutDevice() {
        return mAddOnLayoutDevices;
    }

    public List<LayoutDevice> getUserLayoutDevices() {
        return mUserLayoutDevices;
    }

    public LayoutDevice getUserLayoutDevice(String name) {
        for (LayoutDevice d : mUserLayoutDevices) {
            if (d.getName().equals(name)) {
                return d;
            }
        }

        return null;
    }

    public LayoutDevice addUserDevice(String name, float xdpi, float ydpi) {
        LayoutDevice d = new LayoutDevice(name);
        d.setXDpi(xdpi);
        d.setYDpi(ydpi);
        mUserLayoutDevices.add(d);
        combineLayoutDevices();

        return d;
    }

    public void removeUserDevice(LayoutDevice device) {
        if (mUserLayoutDevices.remove(device)) {
            combineLayoutDevices();
        }
    }

    /**
     * Replaces a device with a new one with new name and/or x/y dpi, and return the new device.
     * If the name and dpi values are identical the given device is returned an nothing is done
     * @param device the {@link LayoutDevice} to replace
     * @param newName the new name.
     * @param newXDpi the new X dpi value
     * @param newYDpi the new Y dpi value.
     * @return the new LayoutDevice
     */
    public LayoutDevice replaceUserDevice(LayoutDevice device, String newName,
            float newXDpi, float newYDpi) {
        if (device.getName().equals(newName) && device.getXDpi() == newXDpi &&
                device.getYDpi() == newYDpi) {
            return device;
        }

        // else create a new device
        LayoutDevice newDevice = new LayoutDevice(newName);
        newDevice.setXDpi(newXDpi);
        newDevice.setYDpi(newYDpi);

        // and get the Folderconfiguration
        List<DeviceConfig> configs = device.getConfigs();
        newDevice.addConfigs(configs);

        // replace the old device with the new
        mUserLayoutDevices.remove(device);
        mUserLayoutDevices.add(newDevice);
        combineLayoutDevices();

        return newDevice;
    }

    /**
     * Adds or replaces a configuration in a given {@link LayoutDevice}.
     * @param device the device to modify
     * @param configName the configuration name to add or replace
     * @param config the configuration to set
     */
    public void addUserConfiguration(LayoutDevice device, String configName,
            FolderConfiguration config) {
        // check that the device does belong to the user list.
        // the main goal is to make sure that this does not belong to the default/addon list.
        if (mUserLayoutDevices.contains(device)) {
            device.addConfig(configName, config);
        }
    }

    /**
     * Replaces a configuration in a given {@link LayoutDevice}.
     * @param device the device to modify
     * @param oldConfigName the name of the config to replace. If null, the new config is simply
     * added.
     * @param newConfigName the configuration name to add or replace
     * @param config the configuration to set
     */
    public void replaceUserConfiguration(LayoutDevice device, String oldConfigName,
            String newConfigName, FolderConfiguration config) {
        // check that the device does belong to the user list.
        // the main goal is to make sure that this does not belong to the default/addon list.
        if (mUserLayoutDevices.contains(device)) {
            // if the old and new config name are different, remove the old one
            if (oldConfigName != null && oldConfigName.equals(newConfigName) == false) {
                device.removeConfig(oldConfigName);
            }

            // and then add the new one
            device.addConfig(newConfigName, config);
        }
    }

    /**
     * Removes a configuration from a given user {@link LayoutDevice}
     * @param device the device to modify
     * @param configName the name of the config to remove
     */
    public void removeUserConfiguration(LayoutDevice device, String configName) {
        // check that the device does belong to the user list.
        // the main goal is to make sure that this does not belong to the default/addon list.
        if (mUserLayoutDevices.contains(device)) {
            device.removeConfig(configName);
        }
    }

    /**
     * Saves the user-made {@link LayoutDevice}s to disk.
     */
    public void save() {
        try {
            String userFolder = AndroidLocation.getFolder();
            File deviceXml = new File(userFolder, SdkConstants.FN_DEVICES_XML);
            if (deviceXml.isDirectory() == false) {
                write(deviceXml, mUserLayoutDevices);
            }
        } catch (AndroidLocationException e) {
            // no user folder? simply don't save the user layout device.
            // we could display the error, but it's likely something else did before, as
            // nothing will work w/o it.
            AdtPlugin.log(e, "Unable to find user directory");
        }
    }

    /**
     * Loads the default built-in and user created Layout Devices.
     * @param sdkOsLocation location of the SDK.
     */
    void loadDefaultAndUserDevices(String sdkOsLocation) {
        // load the default devices
        loadDefaultLayoutDevices(sdkOsLocation);

        // load the user devices;
        try {
            String userFolder = AndroidLocation.getFolder();
            File deviceXml = new File(userFolder, SdkConstants.FN_DEVICES_XML);
            if (deviceXml.isFile()) {
                parseLayoutDevices(deviceXml, mUserLayoutDevices);
            }
        } catch (AndroidLocationException e) {
            // no user folder? simply don't load the user layout device
            AdtPlugin.log(e, "Unable to find user directory");
        }
    }

    void parseAddOnLayoutDevice(File deviceXml) {
        parseLayoutDevices(deviceXml, mAddOnLayoutDevices);
    }

    void sealAddonLayoutDevices() {
        mAddOnLayoutDevices = Collections.unmodifiableList(mAddOnLayoutDevices);

        combineLayoutDevices();
    }

    /**
     * Does the actual parsing of a devices.xml file.
     * @param deviceXml the {@link File} to load/parse. This must be an existing file.
     * @param list the list in which to write the parsed {@link LayoutDevice}.
     */
    private void parseLayoutDevices(File deviceXml, List<LayoutDevice> list) {
        // first we validate the XML
        try {
            Source source = new StreamSource(new FileReader(deviceXml));

            CaptureErrorHandler errorHandler = new CaptureErrorHandler(deviceXml.getAbsolutePath());

            Validator validator = LayoutDevicesXsd.getValidator(errorHandler);
            validator.validate(source);

            if (errorHandler.foundError() == false) {
                // do the actual parsing
                LayoutDeviceHandler handler = new LayoutDeviceHandler();

                SAXParser parser = mParserFactory.newSAXParser();
                parser.parse(new InputSource(new FileInputStream(deviceXml)), handler);

                // get the parsed devices
                list.addAll(handler.getDevices());
            }
        } catch (SAXException e) {
            AdtPlugin.log(e, "Error parsing %1$s", deviceXml.getAbsoluteFile());
        } catch (FileNotFoundException e) {
            // this shouldn't happen as we check above.
        } catch (IOException e) {
            AdtPlugin.log(e, "Error reading %1$s", deviceXml.getAbsoluteFile());
        } catch (ParserConfigurationException e) {
            AdtPlugin.log(e, "Error parsing %1$s", deviceXml.getAbsoluteFile());
        }
    }

    /**
     * Creates some built-it layout devices.
     */
    private void loadDefaultLayoutDevices(String sdkOsLocation) {
        ArrayList<LayoutDevice> list = new ArrayList<LayoutDevice>();
        File toolsFolder = new File(sdkOsLocation, SdkConstants.OS_SDK_TOOLS_LIB_FOLDER);
        if (toolsFolder.isDirectory()) {
            File deviceXml = new File(toolsFolder, SdkConstants.FN_DEVICES_XML);
            if (deviceXml.isFile()) {
                parseLayoutDevices(deviceXml, list);
            }
        }
        mDefaultLayoutDevices = Collections.unmodifiableList(list);
    }

    private void combineLayoutDevices() {
        ArrayList<LayoutDevice> list = new ArrayList<LayoutDevice>();
        list.addAll(mDefaultLayoutDevices);
        list.addAll(mAddOnLayoutDevices);
        list.addAll(mUserLayoutDevices);

        mLayoutDevices = Collections.unmodifiableList(list);
    }

    /**
     * Writes the given {@link LayoutDevice}s into the given file.
     * @param deviceXml the file to write.
     * @param deviceList the LayoutDevice to write into the file.
     */
    private void write(File deviceXml, List<LayoutDevice> deviceList) {
        try {
            // create a new document
            DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
            docFactory.setNamespaceAware(true);
            DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
            Document doc = docBuilder.newDocument();

            // create a base node
            Element baseNode = doc.createElementNS(
                    LayoutDevicesXsd.NS_LAYOUT_DEVICE_XSD,
                    LayoutDevicesXsd.NODE_LAYOUT_DEVICES);
            // create the prefix for the namespace
            baseNode.setPrefix("d");
            doc.appendChild(baseNode);

            // fill it with the layout devices.
            for (LayoutDevice device : deviceList) {
                device.saveTo(doc, baseNode);
            }

            // save the document to disk
            // Prepare the DOM document for writing
            Source source = new DOMSource(doc);

            // Prepare the output file
            File file = new File(deviceXml.getAbsolutePath());
            Result result = new StreamResult(file);

            // Write the DOM document to the file
            Transformer xformer = TransformerFactory.newInstance().newTransformer();
            xformer.transform(source, result);
        } catch (Exception e) {
            AdtPlugin.log(e, "Failed to write %s", deviceXml.getAbsolutePath());
        }
    }
}
