/*
* Copyright (C) 2015 Samsung System LSI
* 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.bluetooth.map;

import android.util.Log;
import android.util.Xml;

import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.XmlUtils;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Locale;


/**
 * Class to contain a single folder element representation.
 *
 */
public class BluetoothMapFolderElement implements Comparable<BluetoothMapFolderElement> {
    private String mName;
    private BluetoothMapFolderElement mParent = null;
    private long mFolderId = -1;
    private boolean mHasSmsMmsContent = false;
    private boolean mHasImContent = false;
    private boolean mHasEmailContent = false;

    private boolean mIgnore = false;

    private HashMap<String, BluetoothMapFolderElement> mSubFolders;

    private static final boolean D = BluetoothMapService.DEBUG;
    private static final boolean V = BluetoothMapService.VERBOSE;

    private static final String TAG = "BluetoothMapFolderElement";

    public BluetoothMapFolderElement(String name, BluetoothMapFolderElement parrent) {
        this.mName = name;
        this.mParent = parrent;
        mSubFolders = new HashMap<String, BluetoothMapFolderElement>();
    }

    public void setIngore(boolean ignore) {
        mIgnore = ignore;
    }

    public boolean shouldIgnore() {
        return mIgnore;
    }

    public String getName() {
        return mName;
    }

    public boolean hasSmsMmsContent() {
        return mHasSmsMmsContent;
    }

    public long getFolderId() {
        return mFolderId;
    }

    public boolean hasEmailContent() {
        return mHasEmailContent;
    }

    public void setFolderId(long folderId) {
        this.mFolderId = folderId;
    }

    public void setHasSmsMmsContent(boolean hasSmsMmsContent) {
        this.mHasSmsMmsContent = hasSmsMmsContent;
    }

    public void setHasEmailContent(boolean hasEmailContent) {
        this.mHasEmailContent = hasEmailContent;
    }

    public void setHasImContent(boolean hasImContent) {
        this.mHasImContent = hasImContent;
    }

    public boolean hasImContent() {
        return mHasImContent;
    }

    /**
     * Fetch the parent folder.
     * @return the parent folder or null if we are at the root folder.
     */
    public BluetoothMapFolderElement getParent() {
        return mParent;
    }

    /**
     * Build the full path to this folder
     * @return a string representing the full path.
     */
    public String getFullPath() {
        StringBuilder sb = new StringBuilder(mName);
        BluetoothMapFolderElement current = mParent;
        while (current != null) {
            if (current.getParent() != null) {
                sb.insert(0, current.mName + "/");
            }
            current = current.getParent();
        }
        //sb.insert(0, "/"); Should this be included? The MAP spec. do not include it in examples.
        return sb.toString();
    }


    public BluetoothMapFolderElement getFolderByName(String name) {
        BluetoothMapFolderElement folderElement = this.getRoot();
        folderElement = folderElement.getSubFolder("telecom");
        folderElement = folderElement.getSubFolder("msg");
        folderElement = folderElement.getSubFolder(name);
        if (folderElement != null && folderElement.getFolderId() == -1) {
            folderElement = null;
        }
        return folderElement;
    }

    public BluetoothMapFolderElement getFolderById(long id) {
        return getFolderById(id, this);
    }

    public static BluetoothMapFolderElement getFolderById(long id,
            BluetoothMapFolderElement folderStructure) {
        if (folderStructure == null) {
            return null;
        }
        return findFolderById(id, folderStructure.getRoot());
    }

    private static BluetoothMapFolderElement findFolderById(long id,
            BluetoothMapFolderElement folder) {
        if (folder.getFolderId() == id) {
            return folder;
        }
        /* Else */
        for (BluetoothMapFolderElement subFolder : folder.mSubFolders.values()
                .toArray(new BluetoothMapFolderElement[folder.mSubFolders.size()])) {
            BluetoothMapFolderElement ret = findFolderById(id, subFolder);
            if (ret != null) {
                return ret;
            }
        }
        return null;
    }


    /**
     * Fetch the root folder.
     * @return the root folder.
     */
    public BluetoothMapFolderElement getRoot() {
        BluetoothMapFolderElement rootFolder = this;
        while (rootFolder.getParent() != null) {
            rootFolder = rootFolder.getParent();
        }
        return rootFolder;
    }

    /**
     * Add a virtual folder.
     * @param name the name of the folder to add.
     * @return the added folder element.
     */
    public BluetoothMapFolderElement addFolder(String name) {
        name = name.toLowerCase(Locale.US);
        BluetoothMapFolderElement newFolder = mSubFolders.get(name);
        if (newFolder == null) {
            if (D) {
                Log.i(TAG, "addFolder():" + name);
            }
            newFolder = new BluetoothMapFolderElement(name, this);
            mSubFolders.put(name, newFolder);
        } else {
            if (D) {
                Log.i(TAG, "addFolder():" + name + " already added");
            }
        }
        return newFolder;
    }

    /**
     * Add a sms/mms folder.
     * @param name the name of the folder to add.
     * @return the added folder element.
     */
    public BluetoothMapFolderElement addSmsMmsFolder(String name) {
        if (D) {
            Log.i(TAG, "addSmsMmsFolder()");
        }
        BluetoothMapFolderElement newFolder = addFolder(name);
        newFolder.setHasSmsMmsContent(true);
        return newFolder;
    }

    /**
     * Add a im folder.
     * @param name the name of the folder to add.
     * @return the added folder element.
     */
    public BluetoothMapFolderElement addImFolder(String name, long idFolder) {
        if (D) {
            Log.i(TAG, "addImFolder() id = " + idFolder);
        }
        BluetoothMapFolderElement newFolder = addFolder(name);
        newFolder.setHasImContent(true);
        newFolder.setFolderId(idFolder);
        return newFolder;
    }

    /**
     * Add an Email folder.
     * @param name the name of the folder to add.
     * @return the added folder element.
     */
    public BluetoothMapFolderElement addEmailFolder(String name, long emailFolderId) {
        if (V) {
            Log.v(TAG, "addEmailFolder() id = " + emailFolderId);
        }
        BluetoothMapFolderElement newFolder = addFolder(name);
        newFolder.setFolderId(emailFolderId);
        newFolder.setHasEmailContent(true);
        return newFolder;
    }

    /**
     * Fetch the number of sub folders.
     * @return returns the number of sub folders.
     */
    public int getSubFolderCount() {
        return mSubFolders.size();
    }

    /**
     * Returns the subFolder element matching the supplied folder name.
     * @param folderName the name of the subFolder to find.
     * @return the subFolder element if found {@code null} otherwise.
     */
    public BluetoothMapFolderElement getSubFolder(String folderName) {
        return mSubFolders.get(folderName.toLowerCase());
    }

    public byte[] encode(int offset, int count) throws UnsupportedEncodingException {
        StringWriter sw = new StringWriter();
        XmlSerializer xmlMsgElement = new FastXmlSerializer();
        int i, stopIndex;
        // We need index based access to the subFolders
        BluetoothMapFolderElement[] folders =
                mSubFolders.values().toArray(new BluetoothMapFolderElement[mSubFolders.size()]);

        if (offset > mSubFolders.size()) {
            throw new IllegalArgumentException("FolderListingEncode: offset > subFolders.size()");
        }

        stopIndex = offset + count;
        if (stopIndex > mSubFolders.size()) {
            stopIndex = mSubFolders.size();
        }

        try {
            xmlMsgElement.setOutput(sw);
            xmlMsgElement.startDocument("UTF-8", true);
            xmlMsgElement.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
            xmlMsgElement.startTag(null, "folder-listing");
            xmlMsgElement.attribute(null, "version", BluetoothMapUtils.MAP_V10_STR);
            for (i = offset; i < stopIndex; i++) {
                xmlMsgElement.startTag(null, "folder");
                xmlMsgElement.attribute(null, "name", folders[i].getName());
                xmlMsgElement.endTag(null, "folder");
            }
            xmlMsgElement.endTag(null, "folder-listing");
            xmlMsgElement.endDocument();
        } catch (IllegalArgumentException e) {
            if (D) {
                Log.w(TAG, e);
            }
            throw new IllegalArgumentException("error encoding folderElement");
        } catch (IllegalStateException e) {
            if (D) {
                Log.w(TAG, e);
            }
            throw new IllegalArgumentException("error encoding folderElement");
        } catch (IOException e) {
            if (D) {
                Log.w(TAG, e);
            }
            throw new IllegalArgumentException("error encoding folderElement");
        }
        return sw.toString().getBytes("UTF-8");
    }

    /* The functions below are useful for implementing a MAP client, reusing the object.
     * Currently they are only used for test purposes.
     * */

    /**
     * Append sub folders from an XML document as specified in the MAP specification.
     * Attributes will be inherited from parent folder - with regards to message types in the
     * folder.
     * @param xmlDocument - InputStream with the document
     *
     * @throws XmlPullParserException
     * @throws IOException
     */
    public void appendSubfolders(InputStream xmlDocument)
            throws XmlPullParserException, IOException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            int type;
            parser.setInput(xmlDocument, "UTF-8");

            // First find the folder-listing
            while ((type = parser.next()) != XmlPullParser.END_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                // Skip until we get a start tag
                if (parser.getEventType() != XmlPullParser.START_TAG) {
                    continue;
                }
                // Skip until we get a folder-listing tag
                String name = parser.getName();
                if (!name.equalsIgnoreCase("folder-listing")) {
                    if (D) {
                        Log.i(TAG, "Unknown XML tag: " + name);
                    }
                    XmlUtils.skipCurrentTag(parser);
                }
                readFolders(parser);
            }
        } finally {
            xmlDocument.close();
        }
    }

    /**
     * Parses folder elements, and add to mSubFolders.
     * @param parser the Xml Parser currently pointing to an folder-listing tag.
     * @throws XmlPullParserException
     * @throws IOException
     */
    public void readFolders(XmlPullParser parser) throws XmlPullParserException, IOException {
        int type;
        if (D) {
            Log.i(TAG, "readFolders(): ");
        }
        while ((type = parser.next()) != XmlPullParser.END_TAG
                && type != XmlPullParser.END_DOCUMENT) {
            // Skip until we get a start tag
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }
            // Skip until we get a folder-listing tag
            String name = parser.getName();
            if (!name.trim().equalsIgnoreCase("folder")) {
                if (D) {
                    Log.i(TAG, "Unknown XML tag: " + name);
                }
                XmlUtils.skipCurrentTag(parser);
                continue;
            }
            int count = parser.getAttributeCount();
            for (int i = 0; i < count; i++) {
                if (parser.getAttributeName(i).trim().equalsIgnoreCase("name")) {
                    // We found a folder, append to sub folders.
                    BluetoothMapFolderElement element =
                            addFolder(parser.getAttributeValue(i).trim());
                    element.setHasEmailContent(mHasEmailContent);
                    element.setHasImContent(mHasImContent);
                    element.setHasSmsMmsContent(mHasSmsMmsContent);
                } else {
                    if (D) {
                        Log.i(TAG, "Unknown XML attribute: " + parser.getAttributeName(i));
                    }
                }
            }
            parser.nextTag();
        }
    }

    /**
     * Recursive compare of all folder names
     */
    @Override
    public int compareTo(BluetoothMapFolderElement another) {
        if (another == null) {
            return 1;
        }
        int ret = mName.compareToIgnoreCase(another.mName);
        // TODO: Do we want to add compare of folder type?
        if (ret == 0) {
            ret = mSubFolders.size() - another.mSubFolders.size();
            if (ret == 0) {
                // Compare all sub folder elements (will do nothing if mSubFolders is empty)
                for (BluetoothMapFolderElement subfolder : mSubFolders.values()) {
                    BluetoothMapFolderElement subfolderAnother =
                            another.mSubFolders.get(subfolder.getName());
                    if (subfolderAnother == null) {
                        if (D) {
                            Log.i(TAG, subfolder.getFullPath() + " not in another");
                        }
                        return 1;
                    }
                    ret = subfolder.compareTo(subfolderAnother);
                    if (ret != 0) {
                        if (D) {
                            Log.i(TAG, subfolder.getFullPath() + " filed compareTo()");
                        }
                        return ret;
                    }
                }
            } else {
                if (D) {
                    Log.i(TAG, "mSubFolders.size(): " + mSubFolders.size()
                            + " another.mSubFolders.size(): " + another.mSubFolders.size());
                }
            }
        } else {
            if (D) {
                Log.i(TAG, "mName: " + mName + " another.mName: " + another.mName);
            }
        }
        return ret;
    }
}
