/*
 * Copyright (C) 2008 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.wizards.newxmlfile;

import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.IconFactory;
import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatPreferences;
import com.android.ide.eclipse.adt.internal.editors.formatting.XmlFormatStyle;
import com.android.ide.eclipse.adt.internal.editors.formatting.XmlPrettyPrinter;
import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileCreationPage.TypeInfo;
import com.android.resources.ResourceFolderType;
import com.android.util.Pair;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.INewWizard;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PartInitException;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;

/**
 * The "New Android XML File Wizard" provides the ability to create skeleton XML
 * resources files for Android projects.
 * <p/>
 * The wizard has one page, {@link NewXmlFileCreationPage}, used to select the project,
 * the resource folder, resource type and file name. It then creates the XML file.
 */
public class NewXmlFileWizard extends Wizard implements INewWizard {
    /** The XML header to write at the top of the XML file */
    public static final String XML_HEADER_LINE = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$

    private static final String PROJECT_LOGO_LARGE = "android-64"; //$NON-NLS-1$

    protected static final String MAIN_PAGE_NAME = "newAndroidXmlFilePage"; //$NON-NLS-1$

    private NewXmlFileCreationPage mMainPage;
    private ChooseConfigurationPage mConfigPage;
    private Values mValues;

    public void init(IWorkbench workbench, IStructuredSelection selection) {
        setHelpAvailable(false); // TODO have help
        setWindowTitle("New Android XML File");
        setImageDescriptor();

        mValues = new Values();
        mMainPage = createMainPage(mValues);
        mMainPage.setTitle("New Android XML File");
        mMainPage.setDescription("Creates a new Android XML file.");
        mMainPage.setInitialSelection(selection);

        mConfigPage = new ChooseConfigurationPage(mValues);
    }

    /**
     * Creates the wizard page.
     * <p/>
     * Please do NOT override this method.
     * <p/>
     * This is protected so that it can be overridden by unit tests.
     * However the contract of this class is private and NO ATTEMPT will be made
     * to maintain compatibility between different versions of the plugin.
     */
    protected NewXmlFileCreationPage createMainPage(NewXmlFileWizard.Values values) {
        return new NewXmlFileCreationPage(MAIN_PAGE_NAME, values);
    }

    // -- Methods inherited from org.eclipse.jface.wizard.Wizard --
    //
    // The Wizard class implements most defaults and boilerplate code needed by
    // IWizard

    /**
     * Adds pages to this wizard.
     */
    @Override
    public void addPages() {
        addPage(mMainPage);
        addPage(mConfigPage);

    }

    /**
     * Performs any actions appropriate in response to the user having pressed
     * the Finish button, or refuse if finishing now is not permitted: here, it
     * actually creates the workspace project and then switch to the Java
     * perspective.
     *
     * @return True
     */
    @Override
    public boolean performFinish() {
        final Pair<IFile, IRegion> created = createXmlFile();
        if (created == null) {
            return false;
        } else {
            // Open the file
            // This has to be delayed in order for focus handling to work correctly
            AdtPlugin.getDisplay().asyncExec(new Runnable() {
                public void run() {
                    IFile file = created.getFirst();
                    IRegion region = created.getSecond();
                    try {
                        IEditorPart editor = AdtPlugin.openFile(file, null,
                                false /*showEditorTab*/);
                        if (editor instanceof AndroidXmlEditor) {
                            final AndroidXmlEditor xmlEditor = (AndroidXmlEditor)editor;
                            if (!xmlEditor.hasMultiplePages()) {
                                xmlEditor.show(region.getOffset(), region.getLength(),
                                        true /* showEditorTab */);
                            }
                        }
                    } catch (PartInitException e) {
                        AdtPlugin.log(e, "Failed to create %1$s: missing type", //$NON-NLS-1$
                                file.getFullPath().toString());
                    }
                }});

            return true;
        }
    }

    // -- Custom Methods --

    private Pair<IFile, IRegion> createXmlFile() {
        IFile file = mValues.getDestinationFile();
        TypeInfo type = mValues.type;
        if (type == null) {
            // this is not expected to happen
            String name = file.getFullPath().toString();
            AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing type", name);  //$NON-NLS-1$
            return null;
        }
        String xmlns = type.getXmlns();
        String root = mMainPage.getRootElement();
        if (root == null) {
            // this is not expected to happen
            AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing root element", //$NON-NLS-1$
                    file.toString());
            return null;
        }

        String attrs = type.getDefaultAttrs(mValues.project, root);
        String child = type.getChild(mValues.project, root);
        return createXmlFile(file, xmlns, root, attrs, child, type.getResFolderType());
    }

    /** Creates a new file using the given root element, namespace and root attributes */
    private static Pair<IFile, IRegion> createXmlFile(IFile file, String xmlns,
            String root, String rootAttributes, String child, ResourceFolderType folderType) {
        String name = file.getFullPath().toString();
        boolean need_delete = false;

        if (file.exists()) {
            if (!AdtPlugin.displayPrompt("New Android XML File",
                String.format("Do you want to overwrite the file %1$s ?", name))) {
                // abort if user selects cancel.
                return null;
            }
            need_delete = true;
        } else {
            createWsParentDirectory(file.getParent());
        }

        StringBuilder sb = new StringBuilder(XML_HEADER_LINE);

        sb.append('<').append(root);
        if (xmlns != null) {
            sb.append('\n').append("  xmlns:android=\"").append(xmlns).append('"');  //$NON-NLS-1$
        }

        if (rootAttributes != null) {
            sb.append("\n  ");                       //$NON-NLS-1$
            sb.append(rootAttributes.replace("\n", "\n  "));  //$NON-NLS-1$ //$NON-NLS-2$
        }

        sb.append(">\n");                            //$NON-NLS-1$

        if (child != null) {
            sb.append(child);
        }

        boolean autoFormat = AdtPrefs.getPrefs().getUseCustomXmlFormatter();

        // Insert an indented caret. Since the markup here will be reformatted, we need to
        // insert text tokens that the formatter will preserve, which we can then turn back
        // into indentation and a caret offset:
        final String indentToken = "${indent}"; //$NON-NLS-1$
        final String caretToken = "${caret}";   //$NON-NLS-1$
        sb.append(indentToken);
        sb.append(caretToken);
        if (!autoFormat) {
            sb.append('\n');
        }

        sb.append("</").append(root).append(">\n");  //$NON-NLS-1$ //$NON-NLS-2$

        XmlFormatPreferences formatPrefs = XmlFormatPreferences.create();
        String fileContents;
        if (!autoFormat) {
            fileContents = sb.toString();
        } else {
            XmlFormatStyle style = XmlFormatStyle.getForFolderType(folderType);
            fileContents = XmlPrettyPrinter.prettyPrint(sb.toString(), formatPrefs,
                                style, null /*lineSeparator*/);
        }

        // Remove marker tokens and replace them with whitespace
        fileContents = fileContents.replace(indentToken, formatPrefs.getOneIndentUnit());
        int caretOffset = fileContents.indexOf(caretToken);
        if (caretOffset != -1) {
            fileContents = fileContents.replace(caretToken, ""); //$NON-NLS-1$
        }

        String error = null;
        try {
            byte[] buf = fileContents.getBytes("UTF8");    //$NON-NLS-1$
            InputStream stream = new ByteArrayInputStream(buf);
            if (need_delete) {
                file.delete(IResource.KEEP_HISTORY | IResource.FORCE, null /*monitor*/);
            }
            file.create(stream, true /*force*/, null /*progress*/);
            IRegion region = caretOffset != -1 ? new Region(caretOffset, 0) : null;
            return Pair.of(file, region);
        } catch (UnsupportedEncodingException e) {
            error = e.getMessage();
        } catch (CoreException e) {
            error = e.getMessage();
        }

        error = String.format("Failed to generate %1$s: %2$s", name, error);
        AdtPlugin.displayError("New Android XML File", error);
        return null;
    }

    /**
     * Returns true if the New XML Wizard can create new files of the given
     * {@link ResourceFolderType}
     *
     * @param folderType the folder type to create a file for
     * @return true if this wizard can create new files for the given folder type
     */
    public static boolean canCreateXmlFile(ResourceFolderType folderType) {
        TypeInfo typeInfo = NewXmlFileCreationPage.getTypeInfo(folderType);
        return typeInfo != null && (typeInfo.getDefaultRoot(null /*project*/) != null ||
                typeInfo.getRootSeed() instanceof String);
    }

    /**
     * Creates a new XML file using the template according to the given folder type
     *
     * @param project the project to create the file in
     * @param file the file to be created
     * @param folderType the type of folder to look up a template for
     * @return the created file
     */
    public static Pair<IFile, IRegion> createXmlFile(IProject project, IFile file,
            ResourceFolderType folderType) {
        TypeInfo type = NewXmlFileCreationPage.getTypeInfo(folderType);
        String xmlns = type.getXmlns();
        String root = type.getDefaultRoot(project);
        if (root == null) {
            root = type.getRootSeed().toString();
        }
        String attrs = type.getDefaultAttrs(project, root);
        return createXmlFile(file, xmlns, root, attrs, null, folderType);
    }

    /**
     * Creates all the directories required for the given path.
     *
     * @param wsPath the path to create all the parent directories for
     * @return true if all the parent directories were created
     */
    public static boolean createWsParentDirectory(IContainer wsPath) {
        if (wsPath.getType() == IResource.FOLDER) {
            if (wsPath.exists()) {
                return true;
            }

            IFolder folder = (IFolder) wsPath;
            try {
                if (createWsParentDirectory(wsPath.getParent())) {
                    folder.create(true /* force */, true /* local */, null /* monitor */);
                    return true;
                }
            } catch (CoreException e) {
                e.printStackTrace();
            }
        }

        return false;
    }

    /**
     * Returns an image descriptor for the wizard logo.
     */
    private void setImageDescriptor() {
        ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE);
        setDefaultPageImageDescriptor(desc);
    }

    /**
     * Specific New XML File wizard tied to the {@link ResourceFolderType#LAYOUT} type
     */
    public static class NewLayoutWizard extends NewXmlFileWizard {
        /** Creates a new {@link NewLayoutWizard} */
        public NewLayoutWizard() {
        }

        @Override
        public void init(IWorkbench workbench, IStructuredSelection selection) {
            super.init(workbench, selection);
            setWindowTitle("New Android Layout XML File");
            super.mMainPage.setTitle("New Android Layout XML File");
            super.mMainPage.setDescription("Creates a new Android Layout XML file.");
            super.mMainPage.setInitialFolderType(ResourceFolderType.LAYOUT);
        }
    }

    /**
     * Specific New XML File wizard tied to the {@link ResourceFolderType#VALUES} type
     */
    public static class NewValuesWizard extends NewXmlFileWizard {
        /** Creates a new {@link NewValuesWizard} */
        public NewValuesWizard() {
        }

        @Override
        public void init(IWorkbench workbench, IStructuredSelection selection) {
            super.init(workbench, selection);
            setWindowTitle("New Android Values XML File");
            super.mMainPage.setTitle("New Android Values XML File");
            super.mMainPage.setDescription("Creates a new Android Values XML file.");
            super.mMainPage.setInitialFolderType(ResourceFolderType.VALUES);
        }
    }

    /** Value object which holds the current state of the wizard pages */
    public static class Values {
        /** The currently selected project, or null */
        public IProject project;
        /** The root name of the XML file to create, or null */
        public String name;
        /** The type of XML file to create */
        public TypeInfo type;
        /** The path within the project to create the new file in */
        public String folderPath;
        /** The currently chosen configuration */
        public FolderConfiguration configuration = new FolderConfiguration();

        /**
         * Returns the destination filename or an empty string.
         *
         * @return the filename, never null.
         */
        public String getFileName() {
            String fileName;
            if (name == null) {
                fileName = ""; //$NON-NLS-1$
            } else {
                fileName = name.trim();
                if (fileName.length() > 0 && fileName.indexOf('.') == -1) {
                    fileName = fileName + AdtConstants.DOT_XML;
                }
            }

            return fileName;
        }

        /**
         * Returns a {@link IFile} for the destination file.
         * <p/>
         * Returns null if the project, filename or folder are invalid and the
         * destination file cannot be determined.
         * <p/>
         * The {@link IFile} is a resource. There might or might not be an
         * actual real file.
         *
         * @return an {@link IFile} for the destination file
         */
        public IFile getDestinationFile() {
            String fileName = getFileName();
            if (project != null && folderPath != null && folderPath.length() > 0
                    && fileName.length() > 0) {
                IPath dest = new Path(folderPath).append(fileName);
                IFile file = project.getFile(dest);
                return file;
            }
            return null;
        }
    }
}
