/*
 * Copyright (C) 2012 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.sdklib.devices;

import static com.android.SdkConstants.VALUE_FALSE;
import static com.android.SdkConstants.VALUE_TRUE;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.dvlib.DeviceSchema;
import com.android.resources.Density;
import com.android.resources.Keyboard;
import com.android.resources.KeyboardState;
import com.android.resources.Navigation;
import com.android.resources.NavigationState;
import com.android.resources.ScreenOrientation;
import com.android.resources.ScreenRatio;
import com.android.resources.ScreenRound;
import com.android.resources.ScreenSize;
import com.android.resources.TouchScreen;
import com.android.resources.UiMode;
import com.google.common.base.Splitter;

import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import java.awt.Point;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.validation.Schema;

public class DeviceParser {

    private static class DeviceHandler extends DefaultHandler {
        private static final Splitter sSpaceSplitter = Splitter.on(' ').omitEmptyStrings();
        private static final String ROUND_BOOT_PROP = "ro.emulator.circular";
        private static final String CHIN_BOOT_PROP = "ro.emu.win_outset_bottom_px";

        private final Table<String, String, Device> mDevices = HashBasedTable.create();
        private final StringBuilder mStringAccumulator = new StringBuilder();
        private final File mParentFolder;
        private Meta mMeta;
        private Hardware mHardware;
        private Software mSoftware;
        private State mState;
        private Device.Builder mBuilder;
        private Camera mCamera;
        private Storage.Unit mUnit;
        private String[] mBootProp;

        public DeviceHandler(@Nullable File parentFolder) {
            mParentFolder = parentFolder;
        }

        @NonNull
        public Table<String, String, Device> getDevices() {
            return mDevices;
        }

        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes)
                throws SAXException {

            if (DeviceSchema.NODE_DEVICE.equals(localName)) {
                // Reset everything
                mMeta = null;
                mHardware = null;
                mSoftware = null;
                mState = null;
                mCamera = null;
                mBuilder = new Device.Builder();
            } else if (DeviceSchema.NODE_META.equals(localName)) {
                mMeta = new Meta();
            } else if (DeviceSchema.NODE_HARDWARE.equals(localName)) {
                mHardware = new Hardware();
            } else if (DeviceSchema.NODE_SOFTWARE.equals(localName)) {
                mSoftware = new Software();
            } else if (DeviceSchema.NODE_STATE.equals(localName)) {
                mState = new State();
                // mState can embed a Hardware instance
                mHardware = mHardware.deepCopy();
                String defaultState = attributes.getValue(DeviceSchema.ATTR_DEFAULT);
                if ("true".equals(defaultState) || "1".equals(defaultState)) {
                    mState.setDefaultState(true);
                }
                mState.setName(attributes.getValue(DeviceSchema.ATTR_NAME).trim());
            } else if (DeviceSchema.NODE_CAMERA.equals(localName)) {
                mCamera = new Camera();
            } else if (DeviceSchema.NODE_RAM.equals(localName)
                    || DeviceSchema.NODE_INTERNAL_STORAGE.equals(localName)
                    || DeviceSchema.NODE_REMOVABLE_STORAGE.equals(localName)) {
                mUnit = Storage.Unit.getEnum(attributes.getValue(DeviceSchema.ATTR_UNIT));
            } else if (DeviceSchema.NODE_FRAME.equals(localName)) {
                mMeta.setFrameOffsetLandscape(new Point());
                mMeta.setFrameOffsetPortrait(new Point());
            } else if (DeviceSchema.NODE_SCREEN.equals(localName)) {
                mHardware.setScreen(new Screen());
            } else if (DeviceSchema.NODE_BOOT_PROP.equals(localName)) {
                mBootProp = new String[2];
            }
            mStringAccumulator.setLength(0);
        }

        @Override
        public void characters(char[] ch, int start, int length) {
            mStringAccumulator.append(ch, start, length);
        }

        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            if (DeviceSchema.NODE_DEVICE.equals(localName)) {
                Device device = mBuilder.build();
                mDevices.put(device.getId(), device.getManufacturer(), device);
            } else if (DeviceSchema.NODE_NAME.equals(localName)) {
                mBuilder.setName(getString(mStringAccumulator));
            } else if (DeviceSchema.NODE_ID.equals(localName)) {
                mBuilder.setId(getString(mStringAccumulator));
            } else if (DeviceSchema.NODE_MANUFACTURER.equals(localName)) {
                mBuilder.setManufacturer(getString(mStringAccumulator));
            } else if (DeviceSchema.NODE_META.equals(localName)) {
                mBuilder.setMeta(mMeta);
            } else if (DeviceSchema.NODE_SOFTWARE.equals(localName)) {
                mBuilder.addSoftware(mSoftware);
            } else if (DeviceSchema.NODE_STATE.equals(localName)) {
                mState.setHardware(mHardware);
                mBuilder.addState(mState);
            } else if (DeviceSchema.NODE_SIXTY_FOUR.equals(localName)) {
                mMeta.setIconSixtyFour(new File(mParentFolder, getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_SIXTEEN.equals(localName)) {
                mMeta.setIconSixteen(new File(mParentFolder, getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_PATH.equals(localName)) {
                mMeta.setFrame(new File(mParentFolder, mStringAccumulator.toString().trim()));
            } else if (DeviceSchema.NODE_PORTRAIT_X_OFFSET.equals(localName)) {
                mMeta.getFrameOffsetPortrait().x = getInteger(mStringAccumulator);
            } else if (DeviceSchema.NODE_PORTRAIT_Y_OFFSET.equals(localName)) {
                mMeta.getFrameOffsetPortrait().y = getInteger(mStringAccumulator);
            } else if (DeviceSchema.NODE_LANDSCAPE_X_OFFSET.equals(localName)) {
                mMeta.getFrameOffsetLandscape().x = getInteger(mStringAccumulator);
            } else if (DeviceSchema.NODE_LANDSCAPE_Y_OFFSET.equals(localName)) {
                mMeta.getFrameOffsetLandscape().y = getInteger(mStringAccumulator);
            } else if (DeviceSchema.NODE_SCREEN_SIZE.equals(localName)) {
                mHardware.getScreen().setSize(ScreenSize.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_DIAGONAL_LENGTH.equals(localName)) {
                mHardware.getScreen().setDiagonalLength(getDouble(mStringAccumulator));
            } else if (DeviceSchema.NODE_PIXEL_DENSITY.equals(localName)) {
                mHardware.getScreen().setPixelDensity(
                        Density.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_SCREEN_RATIO.equals(localName)) {
                mHardware.getScreen().setRatio(
                    ScreenRatio.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_X_DIMENSION.equals(localName)) {
                mHardware.getScreen().setXDimension(getInteger(mStringAccumulator));
            } else if (DeviceSchema.NODE_Y_DIMENSION.equals(localName)) {
                mHardware.getScreen().setYDimension(getInteger(mStringAccumulator));
            } else if (DeviceSchema.NODE_XDPI.equals(localName)) {
                mHardware.getScreen().setXdpi(getDouble(mStringAccumulator));
            } else if (DeviceSchema.NODE_YDPI.equals(localName)) {
                mHardware.getScreen().setYdpi(getDouble(mStringAccumulator));
            } else if (DeviceSchema.NODE_MULTITOUCH.equals(localName)) {
                mHardware.getScreen().setMultitouch(
                        Multitouch.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_MECHANISM.equals(localName)) {
                mHardware.getScreen().setMechanism(
                        TouchScreen.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_SCREEN_TYPE.equals(localName)) {
                mHardware.getScreen().setScreenType(
                        ScreenType.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_NETWORKING.equals(localName)) {
                for (String n : getStringList(mStringAccumulator)) {
                    Network net = Network.getEnum(n);
                    if (net != null) {
                        mHardware.addNetwork(net);
                    }
                }
            } else if (DeviceSchema.NODE_SENSORS.equals(localName)) {
                for (String s : getStringList(mStringAccumulator)) {
                    Sensor sens = Sensor.getEnum(s);
                    if (sens != null) {
                        mHardware.addSensor(sens);
                    }
                }
            } else if (DeviceSchema.NODE_MIC.equals(localName)) {
                mHardware.setHasMic(getBool(mStringAccumulator));
            } else if (DeviceSchema.NODE_CAMERA.equals(localName)) {
                mHardware.addCamera(mCamera);
                mCamera = null;
            } else if (DeviceSchema.NODE_LOCATION.equals(localName)) {
                CameraLocation location = CameraLocation.getEnum(getString(mStringAccumulator));
                if (location != null) {
                    mCamera.setLocation(location);
                }
            } else if (DeviceSchema.NODE_AUTOFOCUS.equals(localName)) {
                mCamera.setFlash(getBool(mStringAccumulator));
            } else if (DeviceSchema.NODE_FLASH.equals(localName)) {
                mCamera.setFlash(getBool(mStringAccumulator));
            } else if (DeviceSchema.NODE_KEYBOARD.equals(localName)) {
                mHardware.setKeyboard(Keyboard.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_NAV.equals(localName)) {
                mHardware.setNav(Navigation.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_RAM.equals(localName)) {
                int val = getInteger(mStringAccumulator);
                mHardware.setRam(new Storage(val, mUnit));
            } else if (DeviceSchema.NODE_BUTTONS.equals(localName)) {
                ButtonType buttonType = ButtonType.getEnum(getString(mStringAccumulator));
                if (buttonType != null) {
                    mHardware.setButtonType(buttonType);
                }
            } else if (DeviceSchema.NODE_INTERNAL_STORAGE.equals(localName)) {
                for (String s : getStringList(mStringAccumulator)) {
                    int val = Integer.parseInt(s);
                    mHardware.addInternalStorage(new Storage(val, mUnit));
                }
            } else if (DeviceSchema.NODE_REMOVABLE_STORAGE.equals(localName)) {
                for (String s : getStringList(mStringAccumulator)) {
                    if (s != null && !s.isEmpty()) {
                        int val = Integer.parseInt(s);
                        mHardware.addRemovableStorage(new Storage(val, mUnit));
                    }
                }
            } else if (DeviceSchema.NODE_CPU.equals(localName)) {
                mHardware.setCpu(getString(mStringAccumulator));
            } else if (DeviceSchema.NODE_GPU.equals(localName)) {
                mHardware.setGpu(getString(mStringAccumulator));
            } else if (DeviceSchema.NODE_ABI.equals(localName)) {
                for (String s : getStringList(mStringAccumulator)) {
                    Abi abi = Abi.getEnum(s);
                    if (abi != null) {
                        mHardware.addSupportedAbi(abi);
                    }
                }
            } else if (DeviceSchema.NODE_DOCK.equals(localName)) {
                for (String s : getStringList(mStringAccumulator)) {
                    UiMode d = UiMode.getEnum(s);
                    if (d != null) {
                        mHardware.addSupportedUiMode(d);
                    }
                }
            } else if (DeviceSchema.NODE_POWER_TYPE.equals(localName)) {
                PowerType type = PowerType.getEnum(getString(mStringAccumulator));
                if (type != null) {
                    mHardware.setChargeType(type);
                }
            } else if (DeviceSchema.NODE_API_LEVEL.equals(localName)) {
                String val = getString(mStringAccumulator);
                // Can be one of 5 forms:
                // 1
                // 1-2
                // 1-
                // -2
                // -
                int index;
                if (val.charAt(0) == '-') {
                    if (val.length() == 1) { // -
                        mSoftware.setMinSdkLevel(0);
                        mSoftware.setMaxSdkLevel(Integer.MAX_VALUE);
                    } else { // -2
                        // Remove the front dash and any whitespace between it
                        // and the upper bound.
                        val = val.substring(1).trim();
                        mSoftware.setMinSdkLevel(0);
                        mSoftware.setMaxSdkLevel(Integer.parseInt(val));
                    }
                } else if ((index = val.indexOf('-')) > 0) {
                    if (index == val.length() - 1) { // 1-
                        // Strip the last dash and any whitespace between it and
                        // the lower bound.
                        val = val.substring(0, val.length() - 1).trim();
                        mSoftware.setMinSdkLevel(Integer.parseInt(val));
                        mSoftware.setMaxSdkLevel(Integer.MAX_VALUE);
                    } else { // 1-2
                        String min = val.substring(0, index).trim();
                        String max = val.substring(index + 1);
                        mSoftware.setMinSdkLevel(Integer.parseInt(min));
                        mSoftware.setMaxSdkLevel(Integer.parseInt(max));
                    }
                } else { // 1
                    int apiLevel = Integer.parseInt(val);
                    mSoftware.setMinSdkLevel(apiLevel);
                    mSoftware.setMaxSdkLevel(apiLevel);
                }
            } else if (DeviceSchema.NODE_LIVE_WALLPAPER_SUPPORT.equals(localName)) {
                mSoftware.setLiveWallpaperSupport(getBool(mStringAccumulator));
            } else if (DeviceSchema.NODE_BLUETOOTH_PROFILES.equals(localName)) {
                for (String s : getStringList(mStringAccumulator)) {
                    BluetoothProfile profile = BluetoothProfile.getEnum(s);
                    if (profile != null) {
                        mSoftware.addBluetoothProfile(profile);
                    }
                }
            } else if (DeviceSchema.NODE_GL_VERSION.equals(localName)) {
                // Guaranteed to be in the form [\d]\.[\d]
                mSoftware.setGlVersion(getString(mStringAccumulator));
            } else if (DeviceSchema.NODE_GL_EXTENSIONS.equals(localName)) {
                mSoftware.addAllGlExtensions(getStringList(mStringAccumulator));
            } else if (DeviceSchema.NODE_DESCRIPTION.equals(localName)) {
                mState.setDescription(getString(mStringAccumulator));
            } else if (DeviceSchema.NODE_SCREEN_ORIENTATION.equals(localName)) {
                mState.setOrientation(ScreenOrientation.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_KEYBOARD_STATE.equals(localName)) {
                mState.setKeyState(KeyboardState.getEnum(getString(mStringAccumulator)));
            } else if (DeviceSchema.NODE_NAV_STATE.equals(localName)) {
                // We have an extra state in our XML for nonav that
                // NavigationState doesn't contain
                String navState = getString(mStringAccumulator);
                if (navState.equals("nonav")) {
                    mState.setNavState(NavigationState.HIDDEN);
                } else {
                    mState.setNavState(NavigationState.getEnum(getString(mStringAccumulator)));
                }
            } else if (DeviceSchema.NODE_STATUS_BAR.equals(localName)) {
                mSoftware.setStatusBar(getBool(mStringAccumulator));

            } else if (DeviceSchema.NODE_TAG_ID.equals(localName)) {
                mBuilder.setTagId(getString(mStringAccumulator));
            } else if (DeviceSchema.NODE_PROP_NAME.equals(localName)) {
                assert mBootProp != null && mBootProp.length == 2;
                mBootProp[0] = getString(mStringAccumulator);
            } else if (DeviceSchema.NODE_PROP_VALUE.equals(localName)) {
                assert mBootProp != null && mBootProp.length == 2;
                mBootProp[1] = mStringAccumulator.toString();
            } else if (DeviceSchema.NODE_BOOT_PROP.equals(localName)) {
                assert mBootProp != null && mBootProp.length == 2 &&
                       mBootProp[0] != null && mBootProp[1] != null;
                mBuilder.addBootProp(mBootProp[0], mBootProp[1]);
                checkAndSetIfRound(mBootProp[0], mBootProp[1]);
                mBootProp = null;
            } else if (DeviceSchema.NODE_SKIN.equals(localName)) {
                String path = getString(mStringAccumulator).replace('/', File.separatorChar);
                mHardware.setSkinFile(new File(path));
            }
        }

        @Override
        public void error(SAXParseException e) throws SAXParseException {
            throw e;
        }

        private void checkAndSetIfRound(String bootPropKey, String bootPropValue) {
            // This is a ugly hack. To keep the existing devices.xmls working, the roundness of the
            // screen is stored in a boot property.
            ScreenRound roundness = null;
            if (ROUND_BOOT_PROP.equals(bootPropKey)) {
                if (VALUE_TRUE.equals(bootPropValue)) {
                    roundness = ScreenRound.ROUND;
                } else if (VALUE_FALSE.equals(bootPropValue)) {
                    roundness = ScreenRound.NOTROUND;
                }
                for (State state : mBuilder.getAllStates()) {
                    state.getHardware().getScreen().setScreenRound(roundness);
                }
            }
            if (CHIN_BOOT_PROP.equals(bootPropKey)) {
                int chin = Integer.parseInt(bootPropValue);
                for (State state : mBuilder.getAllStates()) {
                    state.getHardware().getScreen().setChin(chin);
                }
            }
        }

        private static List<String> getStringList(StringBuilder stringAccumulator) {
            List<String> filteredStrings = new ArrayList<String>();
            for (String s : sSpaceSplitter.split(stringAccumulator)) {
                if (s != null && !s.isEmpty()) {
                    filteredStrings.add(s.trim());
                }
            }
            return filteredStrings;
        }

        private static Boolean getBool(StringBuilder s) {
            return equals(s, "true") || equals(s, "1");
        }

        private static double getDouble(StringBuilder stringAccumulator) {
            return Double.parseDouble(getString(stringAccumulator));
        }

        private static String getString(StringBuilder s) {
            return s.toString().trim();
        }

        private static boolean equals(StringBuilder s, String t) {
            int start = 0;
            int length = s.length();
            while (start < length && Character.isWhitespace(s.charAt(start))) {
                start++;
            }
            if (start == length) {
                return t.isEmpty();
            }

            int end = length;
            while (end > start && Character.isWhitespace(s.charAt(end - 1))) {
                end--;
            }

            if (t.length() != (end - start)) {
                return false;
            }

            for (int i = 0, n = t.length(), j = start; i < n; i++, j++) {
                if (Character.toLowerCase(s.charAt(j)) != Character.toLowerCase(t.charAt(i))) {
                    return false;
                }
            }

            return true;
        }

        private static int getInteger(StringBuilder stringAccumulator) {
            return Integer.parseInt(getString(stringAccumulator));
        }
    }

    private static final int MAX_FILE_LENGTH = 640 * 1024;  // ought to be enough for anybody

    private static final SAXParserFactory sParserFactory;

    static {
        sParserFactory = SAXParserFactory.newInstance();
        sParserFactory.setNamespaceAware(true);
    }

    @NonNull
    public static Table<String, String, Device> parse(@NonNull File devicesFile)
            throws SAXException, ParserConfigurationException, IOException {
        if (devicesFile.length() > MAX_FILE_LENGTH) {
            throw new IOException(String.format(
              "%s (%d B) is longer than limit (%d B)",
              devicesFile.getAbsolutePath(), devicesFile.length(), MAX_FILE_LENGTH));
        }

        // stream closed by parseImpl.
        @SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
        InputStream stream = new FileInputStream(devicesFile);
        return parseImpl(stream, devicesFile.getAbsoluteFile().getParentFile());
    }

    /**
     * This method closes the stream.
     */
    @NonNull
    public static Table<String, String, Device> parse(@NonNull InputStream devices)
            throws SAXException, IOException, ParserConfigurationException {
        return parseImpl(devices, null);
    }

    /**
     * After parsing, this method closes the stream.
     */
    @NonNull
    private static Table<String, String, Device> parseImpl(@NonNull InputStream devices, @Nullable File parentDir)
            throws SAXException, IOException, ParserConfigurationException {
        try {
            if (!devices.markSupported()) {
                //noinspection IOResourceOpenedButNotSafelyClosed
                devices = new BufferedInputStream(devices);  // closed in the finally block.
            }
            devices.mark(MAX_FILE_LENGTH);
            int version = DeviceSchema.getXmlSchemaVersion(devices);
            SAXParser parser = getParser(version);
            DeviceHandler dHandler = new DeviceHandler(parentDir);
            devices.reset();
            parser.parse(devices, dHandler);
            return dHandler.getDevices();
        }
        finally {
            // It's better to close the stream here since we may have created it above.
            devices.close();
        }
    }

    @NonNull
    private static SAXParser getParser(int version) throws ParserConfigurationException, SAXException {
        Schema schema = DeviceSchema.getSchema(version);
        if (schema != null) {
            sParserFactory.setSchema(schema);
        }
        return sParserFactory.newSAXParser();
    }
}
