/*
 * 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 com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.dvlib.DeviceSchema;
import com.android.resources.ScreenOrientation;
import com.android.resources.ScreenRound;

import java.awt.Dimension;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Instances of this class contain the specifications for a device. Use the
 * {@link Builder} class to construct a Device object, or the
 * {@link DeviceParser} if constructing device objects from XML conforming to
 * the {@link DeviceSchema} standards.
 */
public final class Device {

    /** Name of the device */
    @NonNull
    private final String mName;

    /** ID of the device */
    @NonNull
    private final String mId;

    /** Manufacturer of the device */
    @NonNull
    private final String mManufacturer;

    /** A list of software capabilities, one for each API level range */
    @NonNull
    private final List<Software> mSoftware;

    /** A list of phone states (landscape, portrait with keyboard out, etc.) */
    @NonNull
    private final List<State> mState;

    /** Meta information such as icon files and device frames */
    @NonNull
    private final Meta mMeta;

    /** Default state of the device */
    @NonNull
    private final State mDefaultState;

    /** Optional tag-id of the device. */
    @Nullable
    private String mTagId;

    /** Optional boot.props of the device. */
    @NonNull
    private Map<String, String> mBootProps;

    /**
     * Returns the name of the {@link Device}. This is intended to be displayed by the user and
     * can vary over time. For a stable internal name of the device, use {@link #getId} instead.
     *
     * @deprecated Use {@link #getId()} or {@link #getDisplayName()} instead based on whether
     *     a stable identifier or a user visible name is needed
     * @return The name of the {@link Device}.
     */
    @NonNull
    @Deprecated
    public String getName() {
        return mName;
    }

    /**
     * Returns the user visible name of the {@link Device}. This is intended to be displayed by the
     * user and can vary over time. For a stable internal name of the device, use {@link #getId}
     * instead.
     *
     * @return The name of the {@link Device}.
     */
    @NonNull
    public String getDisplayName() {
        return mName;
    }

    /**
     * Returns the id of the {@link Device}.
     *
     * @return The id of the {@link Device}.
     */
    @NonNull
    public String getId() {
        return mId;
    }

    /**
     * Returns the manufacturer of the {@link Device}.
     *
     * @return The name of the manufacturer of the {@link Device}.
     */
    @NonNull
    public String getManufacturer() {
        return mManufacturer;
    }

    /**
     * Returns all of the {@link Software} configurations of the {@link Device}.
     *
     * @return A list of all the {@link Software} configurations.
     */
    @NonNull
    public List<Software> getAllSoftware() {
        return mSoftware;
    }

    /**
     * Returns all of the {@link State}s the {@link Device} can be in.
     *
     * @return A list of all the {@link State}s.
     */
    @NonNull
    public List<State> getAllStates() {
        return mState;
    }

    /**
     * Returns the default {@link Hardware} configuration for the device. This
     * is really just a shortcut for getting the {@link Hardware} on the default
     * {@link State}
     *
     * @return The default {@link Hardware} for the device.
     */
    @NonNull
    public Hardware getDefaultHardware() {
        return mDefaultState.getHardware();
    }

    /**
     * Returns the {@link Meta} object for the device, which contains meta
     * information about the device, such as the location of icons.
     *
     * @return The {@link Meta} object for the {@link Device}.
     */
    @NonNull
    public Meta getMeta() {
        return mMeta;
    }

    /**
     * Returns the default {@link State} of the {@link Device}.
     *
     * @return The default {@link State} of the {@link Device}.
     */
    @NonNull
    public State getDefaultState() {
        return mDefaultState;
    }

    /**
     * Returns the software configuration for the given API version.
     *
     * @param apiVersion
     *            The API version requested.
     * @return The Software instance for the requested API version or null if
     *         the API version is unsupported for this device.
     */
    @Nullable
    public Software getSoftware(int apiVersion) {
        for (Software s : mSoftware) {
            if (apiVersion >= s.getMinSdkLevel() && apiVersion <= s.getMaxSdkLevel()) {
                return s;
            }
        }
        return null;
    }

    /**
     * Returns the state of the device with the given name.
     *
     * @param name
     *            The name of the state requested.
     * @return The State object requested or null if there's no state with the
     *         given name.
     */
    @Nullable
    public State getState(String name) {
        for (State s : getAllStates()) {
            if (s.getName().equals(name)) {
                return s;
            }
        }
        return null;
    }

    @SuppressWarnings("SuspiciousNameCombination") // Deliberately swapping orientations
    @Nullable
    public Dimension getScreenSize(@NonNull ScreenOrientation orientation) {
        Screen screen = getDefaultHardware().getScreen();
        if (screen == null) {
            return null;
        }

        // compute width and height to take orientation into account.
        int x = screen.getXDimension();
        int y = screen.getYDimension();
        int screenWidth, screenHeight;

        if (x > y) {
            if (orientation == ScreenOrientation.LANDSCAPE) {
                screenWidth = x;
                screenHeight = y;
            }
            else {
                screenWidth = y;
                screenHeight = x;
            }
        }
        else {
            if (orientation == ScreenOrientation.LANDSCAPE) {
                screenWidth = y;
                screenHeight = x;
            }
            else {
                screenWidth = x;
                screenHeight = y;
            }
        }

        return new Dimension(screenWidth, screenHeight);
    }

    /**
     * Returns the optional tag-id of the device.
     *
     * @return the optional tag-id of the device. Can be null.
     */
    @Nullable
    public String getTagId() {
        return mTagId;
    }

    /**
     * Returns the optional boot.props of the device.
     *
     * @return the optional boot.props of the device. Can be null or empty.
     */
    @NonNull
    public Map<String, String> getBootProps() {
        return mBootProps;
    }

    /**
     * A convenience method to get if the screen for this device is round.
     */
    public boolean isScreenRound() {
        return getDefaultHardware().getScreen().getScreenRound() == ScreenRound.ROUND;
    }

    /**
     * A convenience method to get the chin size for this device.
     */
    public int getChinSize() {
        return getDefaultHardware().getScreen().getChin();
    }

    public static class Builder {
        private String mName;
        private String mId;
        private String mManufacturer;
        private final List<Software> mSoftware = new ArrayList<Software>();
        private final List<State> mState = new ArrayList<State>();
        private Meta mMeta;
        private State mDefaultState;
        private String mTagId;
        private final Map<String, String> mBootProps = new TreeMap<String, String>();

        public Builder() { }

        public Builder(Device d) {
            mTagId = null;
            mName = d.getDisplayName();
            mId = d.getId();
            mManufacturer = d.getManufacturer();
            for (Software s : d.getAllSoftware()) {
                mSoftware.add(s.deepCopy());
            }
            for (State s : d.getAllStates()) {
                mState.add(s.deepCopy());
            }
            mMeta = d.getMeta();
        }

        public void setName(@NonNull String name) {
            mName = name;
        }

        public void setId(@NonNull String id) {
            mId = id;
        }

        public void setTagId(@Nullable String tagId) {
            mTagId = tagId;
        }

        public void addBootProp(@NonNull String propName, @NonNull String propValue) {
            mBootProps.put(propName, propValue);
        }

        public void setManufacturer(@NonNull String manufacturer) {
            mManufacturer = manufacturer;
        }

        public void addSoftware(@NonNull Software sw) {
            mSoftware.add(sw);
        }

        public void addAllSoftware(@NonNull Collection<? extends Software> sw) {
            mSoftware.addAll(sw);
        }

        public void addState(State state) {
            mState.add(state);
        }

        public void addAllState(@NonNull Collection<? extends State> states) {
            mState.addAll(states);
        }

        /**
         * Removes the first {@link State} with the given name
         * @param stateName The name of the {@link State} to remove.
         * @return Whether a {@link State} was removed or not.
         */
        public boolean removeState(@NonNull String stateName) {
            for (int i = 0; i < mState.size(); i++) {
                if (stateName != null && stateName.equals(mState.get(i).getName())) {
                    mState.remove(i);
                    return true;
                }
            }
            return false;
        }

        /**
         * Only for use by the {@link DeviceParser}, so that it can modify the states after they've
         * been added.
         */
        List<State> getAllStates() {
            return mState;
        }

        public void setMeta(@NonNull Meta meta) {
            mMeta = meta;
        }

        public Device build() {
            if (mName == null) {
                throw generateBuildException("Device missing name");
            } else if (mManufacturer == null) {
                throw generateBuildException("Device missing manufacturer");
            } else if (mSoftware.size() <= 0) {
                throw generateBuildException("Device software not configured");
            } else if (mState.size() <= 0) {
                throw generateBuildException("Device states not configured");
            }

            if (mId == null) {
                if ("android-wear".equals(mTagId)) {
                    // b.android.com/200772
                    // When api 21 wear images are removed (hopefully reasonably soon post-api-24),
                    // this also should be, since it is obviously a horrible hack.
                    mId = "wear_" + mName.substring(mName.lastIndexOf(' ') + 1).toLowerCase();
                }
                else {
                    mId = mName;
                }
            }

            if (mMeta == null) {
                mMeta = new Meta();
            }
            for (State s : mState) {
                if (s.isDefaultState()) {
                    mDefaultState = s;
                    break;
                }
            }
            if (mDefaultState == null) {
                throw generateBuildException("Device missing default state");
            }
            return new Device(this);
        }

        private IllegalStateException generateBuildException(String err) {
            String device = "";
            if (mManufacturer != null) {
                device = mManufacturer + ' ';
            }
            if (mName != null) {
                device += mName;
            } else {
                device = "Unknown " + device +"Device";
            }

            return new IllegalStateException("Error building " + device + ": " +err);
        }
    }

    private Device(Builder b) {
        mName = b.mName;
        mId = b.mId;
        mManufacturer = b.mManufacturer;
        mSoftware = Collections.unmodifiableList(b.mSoftware);
        mState = Collections.unmodifiableList(b.mState);
        mMeta = b.mMeta;
        mDefaultState = b.mDefaultState;
        mTagId = b.mTagId;
        mBootProps = Collections.unmodifiableMap(b.mBootProps);
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (!(o instanceof Device)) {
            return false;
        }
        Device d = (Device) o;
        boolean ok = mName.equals(d.getDisplayName())
                && mManufacturer.equals(d.getManufacturer())
                && mSoftware.equals(d.getAllSoftware())
                && mState.equals(d.getAllStates())
                && mMeta.equals(d.getMeta())
                && mDefaultState.equals(d.getDefaultState());
        if (!ok) {
            return false;
        }

        ok = (mTagId == null && d.mTagId == null) ||
             (mTagId != null && mTagId.equals(d.mTagId));
        if (!ok) {
            return false;
        }

        ok = (mBootProps == null && d.mBootProps == null) ||
             (mBootProps != null && mBootProps.equals(d.mBootProps));
        return ok;
    }

    /**
     * For *internal* usage only. Must not be serialized to disk.
     */
    @Override
    public int hashCode() {
        int hash = 17;
        hash = 31 * hash + mName.hashCode();
        hash = 31 * hash + mManufacturer.hashCode();
        hash = 31 * hash + mSoftware.hashCode();
        hash = 31 * hash + mState.hashCode();
        hash = 31 * hash + mMeta.hashCode();
        hash = 31 * hash + mDefaultState.hashCode();

        // tag-id and boot-props are optional and should not change a device's hashcode
        // which did not have them before.
        if (mTagId != null) {
            hash = 31 * hash + mTagId.hashCode();
        }
        if (mBootProps != null && !mBootProps.isEmpty()) {
            hash = 31 * hash + mBootProps.hashCode();
        }
        return hash;
    }

    /** toString value suitable for debugging only. */
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Device [mName=");
        sb.append(mName);
        sb.append(", mId=");
        sb.append(mId);
        sb.append(", mManufacturer=");
        sb.append(mManufacturer);
        sb.append(", mSoftware=");
        sb.append(mSoftware);
        sb.append(", mState=");
        sb.append(mState);
        sb.append(", mMeta=");
        sb.append(mMeta);
        sb.append(", mDefaultState=");
        sb.append(mDefaultState);
        sb.append(", mTagId=");
        sb.append(mTagId);
        sb.append(", mBootProps=");
        sb.append(mBootProps);
        sb.append("]");
        return sb.toString();
    }

    private static Pattern PATTERN = Pattern.compile(
    "(\\d+\\.?\\d*)(?:in|\") (.+?)( \\(.*Nexus.*\\))?"); //$NON-NLS-1$

    /**
     * Returns a "sortable" name for the device -- if a device list is sorted
     * using this sort-aware display name, it will be displayed in an order that
     * is user friendly with devices using names first sorted alphabetically
     * followed by all devices that use a numeric screen size sorted by actual
     * size.
     * <p>
     * Note that although the name results in a proper sort, it is not a name
     * that you actually want to display to the user.
     * <p>
     * Extracted from DeviceMenuListener. Modified to remove the leading space
     * insertion as it doesn't render neatly in the avd manager. Instead added
     * the option to add leading zeroes to make the string names sort properly.
     *
     * Replace "'in'" with '"' (e.g. 2.7" QVGA instead of 2.7in QVGA).
     * Use the same precision for all devices (all but one specify decimals).
     */
    private String getSortableName() {
        String sortableName = mName;
        Matcher matcher = PATTERN.matcher(sortableName);
        if (matcher.matches()) {
            String size = matcher.group(1);
            String n = matcher.group(2);
            int dot = size.indexOf('.');
            if (dot == -1) {
                size = size + ".0";
                dot = size.length() - 2;
            }
            if (dot < 3) {
                // Pad to have at least 3 digits before the dot, for sorting
                // purposes.
                // We can revisit this once we get devices that are more than
                // 999 inches wide.
                size = "000".substring(dot) + size;
            }
            sortableName = size + "\" " + n;
        }

        return sortableName;
    }

    /**
     * Returns a comparator suitable to sort a device list using a sort-aware display name.
     * The list is displayed in an order that is user friendly with devices using names
     * first sorted alphabetically followed by all devices that use a numeric screen size
     * sorted by actual size.
     */
    public static Comparator<Device> getDisplayComparator() {
        return new Comparator<Device>() {
            @Override
            public int compare(Device d1, Device d2) {
                String s1 = d1.getSortableName();
                String s2 = d2.getSortableName();
                if (s1.length() > 1 && s2.length() > 1) {
                    int i1 = Character.isDigit(s1.charAt(0)) ? 1 : 0;
                    int i2 = Character.isDigit(s2.charAt(0)) ? 1 : 0;
                    if (i1 != i2) {
                        return i1 - i2;
                    }
                }
                return s1.compareTo(s2);
            }};
    }

}
