/*
 * Copyright (C) 2010 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.refactorings.renamepackage;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.internal.editors.descriptors.XmlnsAttributeDescriptor;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.xml.AndroidManifest;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.ltk.core.refactoring.Change;
import org.eclipse.ltk.core.refactoring.CompositeChange;
import org.eclipse.ltk.core.refactoring.Refactoring;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
import org.eclipse.ltk.core.refactoring.TextEditChangeGroup;
import org.eclipse.ltk.core.refactoring.TextFileChange;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.text.edits.TextEditGroup;
import org.eclipse.wst.sse.core.StructuredModelManager;
import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 *  Wrapper class defining the stages of the refactoring process
 */
@SuppressWarnings("restriction")
class ApplicationPackageNameRefactoring extends Refactoring {

    private final IProject mProject;
    private final Name mOldPackageName;
    private final Name mNewPackageName;

    List<String> MAIN_COMPONENT_TYPES_LIST = Arrays.asList(MAIN_COMPONENT_TYPES);

    private final static String ANDROID_NS_URI = SdkConstants.NS_RESOURCES;
    private final static String NAMESPACE_DECLARATION_PREFIX =
        XmlnsAttributeDescriptor.XMLNS_COLON;

    ApplicationPackageNameRefactoring(
            IProject project,
            Name oldPackageName,
            Name newPackageName) {
        mProject = project;
        mOldPackageName = oldPackageName;
        mNewPackageName = newPackageName;
    }

    @Override
    public RefactoringStatus checkInitialConditions(IProgressMonitor pm)
            throws CoreException, OperationCanceledException {

        // Accurate refactoring of the "shorthand" names in
        // AndroidManifest.xml depends on not having compilation errors.
        if (mProject.findMaxProblemSeverity(
                IMarker.PROBLEM,
                true,
                IResource.DEPTH_INFINITE) == IMarker.SEVERITY_ERROR) {
            return RefactoringStatus
            .createFatalErrorStatus("Fix the errors in your project, first.");
        }

        return new RefactoringStatus();
    }

    @Override
    public RefactoringStatus checkFinalConditions(IProgressMonitor pm)
            throws OperationCanceledException {

        return new RefactoringStatus();
    }

    @Override
    public Change createChange(IProgressMonitor pm) throws CoreException,
    OperationCanceledException {

        // Traverse all files in the project, building up a list of changes
        JavaFileVisitor fileVisitor = new JavaFileVisitor();
        mProject.accept(fileVisitor);
        return fileVisitor.getChange();
    }

    @Override
    public String getName() {
        return "AndroidPackageNameRefactoring"; //$NON-NLS-1$
    }

    public final static String[] MAIN_COMPONENT_TYPES = {
        AndroidManifest.NODE_ACTIVITY, AndroidManifest.NODE_SERVICE,
        AndroidManifest.NODE_RECEIVER, AndroidManifest.NODE_PROVIDER,
        AndroidManifest.NODE_APPLICATION
    };


    TextEdit updateJavaFileImports(CompilationUnit cu) {

        ImportVisitor importVisitor = new ImportVisitor(cu.getAST());
        cu.accept(importVisitor);
        TextEdit rewrittenImports = importVisitor.getTextEdit();

        // If the import of R was potentially implicit, insert an import statement
        if (cu.getPackage().getName().getFullyQualifiedName()
                .equals(mOldPackageName.getFullyQualifiedName())) {

            ImportRewrite irw = ImportRewrite.create(cu, true);
            irw.addImport(mNewPackageName.getFullyQualifiedName() + '.'
                    + AdtConstants.FN_RESOURCE_BASE);

            try {
                rewrittenImports.addChild( irw.rewriteImports(null) );
            } catch (MalformedTreeException e) {
                Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e);
                AdtPlugin.getDefault().getLog().log(s);
            } catch (CoreException e) {
                Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e);
                AdtPlugin.getDefault().getLog().log(s);
            }
        }

        return rewrittenImports;
    }

    // XML utility functions
    private String stripQuotes(String text) {
        int len = text.length();
        if (len >= 2 && text.charAt(0) == '"' && text.charAt(len - 1) == '"') {
            return text.substring(1, len - 1);
        } else if (len >= 2 && text.charAt(0) == '\'' && text.charAt(len - 1) == '\'') {
            return text.substring(1, len - 1);
        }
        return text;
    }

    private String addQuotes(String text) {
        return '"' + text + '"';
    }

    /*
     * Make the appropriate package name changes to a resource file,
     * e.g. .xml files in res/layout. This entails updating the namespace
     * declarations for custom styleable attributes.  The namespace prefix
     * is user-defined and may be declared in any element where or parent
     * element of where the prefix is used.
     */
    TextFileChange editXmlResourceFile(IFile file) {

        IModelManager modelManager = StructuredModelManager.getModelManager();
        IStructuredDocument sdoc = null;
        try {
            sdoc = modelManager.createStructuredDocumentFor(file);
        } catch (IOException e) {
            Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e);
            AdtPlugin.getDefault().getLog().log(s);
        } catch (CoreException e) {
            Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e);
            AdtPlugin.getDefault().getLog().log(s);
        }

        if (sdoc == null) {
            return null;
        }

        TextFileChange xmlChange = new TextFileChange("XML resource file edit", file);
        xmlChange.setTextType(AdtConstants.EXT_XML);

        MultiTextEdit multiEdit = new MultiTextEdit();
        ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>();

        final String oldAppNamespaceString = String.format(AdtConstants.NS_CUSTOM_RESOURCES,
                mOldPackageName.getFullyQualifiedName());
        final String newAppNamespaceString = String.format(AdtConstants.NS_CUSTOM_RESOURCES,
                mNewPackageName.getFullyQualifiedName());

        // Prepare the change set
        for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) {

            if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
                continue;
            }

            int nb = region.getNumberOfRegions();
            ITextRegionList list = region.getRegions();
            String lastAttrName = null;

            for (int i = 0; i < nb; i++) {
                ITextRegion subRegion = list.get(i);
                String type = subRegion.getType();

                if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
                    // Memorize the last attribute name seen
                    lastAttrName = region.getText(subRegion);

                } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {
                    // Check this is the attribute and the original string

                    if (lastAttrName != null &&
                            lastAttrName.startsWith(NAMESPACE_DECLARATION_PREFIX)) {

                        String lastAttrValue = region.getText(subRegion);
                        if (oldAppNamespaceString.equals(stripQuotes(lastAttrValue))) {

                            // Found an occurrence. Create a change for it.
                            TextEdit edit = new ReplaceEdit(
                                    region.getStartOffset() + subRegion.getStart(),
                                    subRegion.getTextLength(),
                                    addQuotes(newAppNamespaceString));
                            TextEditGroup editGroup = new TextEditGroup(
                                    "Replace package name in custom namespace prefix", edit);

                            multiEdit.addChild(edit);
                            editGroups.add(editGroup);
                        }
                    }
                }
            }
        }

        if (multiEdit.hasChildren()) {
            xmlChange.setEdit(multiEdit);
            for (TextEditGroup group : editGroups) {
                xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, group));
            }

            return xmlChange;
        }
        return null;
    }

    /*
     * Replace all instances of the package name in AndroidManifest.xml.
     * This includes expanding shorthand paths for each Component (Activity,
     * Service, etc.) and of course updating the application package name.
     * The namespace prefix might not be "android", so we resolve it
     * dynamically.
     */
    TextFileChange editAndroidManifest(IFile file) {

        IModelManager modelManager = StructuredModelManager.getModelManager();
        IStructuredDocument sdoc = null;
        try {
            sdoc = modelManager.createStructuredDocumentFor(file);
        } catch (IOException e) {
            Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e);
            AdtPlugin.getDefault().getLog().log(s);
        } catch (CoreException e) {
            Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e);
            AdtPlugin.getDefault().getLog().log(s);
        }

        if (sdoc == null) {
            return null;
        }

        TextFileChange xmlChange = new TextFileChange("Make Manifest edits", file);
        xmlChange.setTextType(AdtConstants.EXT_XML);

        MultiTextEdit multiEdit = new MultiTextEdit();
        ArrayList<TextEditGroup> editGroups = new ArrayList<TextEditGroup>();

        // The namespace prefix is guaranteed to be resolved before
        // the first use of this attribute
        String android_name_attribute = null;

        // Prepare the change set
        for (IStructuredDocumentRegion region : sdoc.getStructuredDocumentRegions()) {

            // Only look at XML "top regions"
            if (!DOMRegionContext.XML_TAG_NAME.equals(region.getType())) {
                continue;
            }

            int nb = region.getNumberOfRegions();
            ITextRegionList list = region.getRegions();
            String lastTagName = null, lastAttrName = null;

            for (int i = 0; i < nb; i++) {
                ITextRegion subRegion = list.get(i);
                String type = subRegion.getType();

                if (DOMRegionContext.XML_TAG_NAME.equals(type)) {
                    // Memorize the last tag name seen
                    lastTagName = region.getText(subRegion);

                } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(type)) {
                    // Memorize the last attribute name seen
                    lastAttrName = region.getText(subRegion);

                } else if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(type)) {

                    String lastAttrValue = region.getText(subRegion);
                    if (lastAttrName != null &&
                            lastAttrName.startsWith(NAMESPACE_DECLARATION_PREFIX)) {

                        // Resolves the android namespace prefix for this file
                        if (ANDROID_NS_URI.equals(stripQuotes(lastAttrValue))) {
                            String android_namespace_prefix = lastAttrName
                                .substring(NAMESPACE_DECLARATION_PREFIX.length());
                            android_name_attribute = android_namespace_prefix + ':'
                                + AndroidManifest.ATTRIBUTE_NAME;
                        }
                    } else if (AndroidManifest.NODE_MANIFEST.equals(lastTagName)
                            && AndroidManifest.ATTRIBUTE_PACKAGE.equals(lastAttrName)) {

                        // Found an occurrence. Create a change for it.
                        TextEdit edit = new ReplaceEdit(region.getStartOffset()
                                + subRegion.getStart(), subRegion.getTextLength(),
                                addQuotes(mNewPackageName.getFullyQualifiedName()));

                        multiEdit.addChild(edit);
                        editGroups.add(new TextEditGroup("Change Android package name", edit));

                    } else if (MAIN_COMPONENT_TYPES_LIST.contains(lastTagName)
                            && lastAttrName != null
                            && lastAttrName.equals(android_name_attribute)) {

                        String package_path = stripQuotes(lastAttrValue);
                        String old_package_name_string = mOldPackageName.getFullyQualifiedName();

                        String absolute_path = AndroidManifest.combinePackageAndClassName(
                                old_package_name_string, package_path);

                        TextEdit edit = new ReplaceEdit(region.getStartOffset()
                                + subRegion.getStart(), subRegion.getTextLength(),
                                addQuotes(absolute_path));

                        multiEdit.addChild(edit);

                        editGroups.add(new TextEditGroup("Update component path", edit));
                    }
                }
            }
        }

        if (multiEdit.hasChildren()) {
            xmlChange.setEdit(multiEdit);
            for (TextEditGroup group : editGroups) {
                xmlChange.addTextEditChangeGroup(new TextEditChangeGroup(xmlChange, group));
            }

            return xmlChange;
        }
        return null;
    }


    /*
     * Iterates through all project files, taking distinct actions based on
     * whether the file is:
     * 1) a .java file (replaces or inserts the "import" statements)
     * 2) a .xml layout file (updates namespace declarations)
     * 3) the AndroidManifest.xml
     */
    class JavaFileVisitor implements IResourceVisitor {

        final List<TextFileChange> mChanges = new ArrayList<TextFileChange>();

        final ASTParser mParser = ASTParser.newParser(AST.JLS3);

        public CompositeChange getChange() {

            Collections.reverse(mChanges);
            CompositeChange change = new CompositeChange("Refactoring Application package name",
                    mChanges.toArray(new Change[mChanges.size()]));
            return change;
        }

        @SuppressWarnings("unused")
        public boolean visit(IResource resource) throws CoreException {
            if (resource instanceof IFile) {
                IFile file = (IFile) resource;
                if (AdtConstants.EXT_JAVA.equals(file.getFileExtension())) {

                    ICompilationUnit icu = JavaCore.createCompilationUnitFrom(file);

                    mParser.setSource(icu);
                    CompilationUnit cu = (CompilationUnit) mParser.createAST(null);

                    TextEdit text_edit = updateJavaFileImports(cu);
                    if (text_edit.hasChildren()) {
                        MultiTextEdit edit = new MultiTextEdit();
                        edit.addChild(text_edit);

                        TextFileChange text_file_change = new TextFileChange(file.getName(), file);
                        text_file_change.setTextType(AdtConstants.EXT_JAVA);
                        text_file_change.setEdit(edit);
                        mChanges.add(text_file_change);
                    }

                    // XXX Partially taken from ExtractStringRefactoring.java
                    // Check this a Layout XML file and get the selection and
                    // its context.
                } else if (AdtConstants.EXT_XML.equals(file.getFileExtension())) {

                    if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName())) {

                        TextFileChange manifest_change = editAndroidManifest(file);
                        mChanges.add(manifest_change);

                    } else {

                        // Currently we only support Android resource XML files,
                        // so they must have a path similar to
                        //   project/res/<type>[-<configuration>]/*.xml
                        // There is no support for sub folders, so the segment count must be 4.
                        // We don't need to check the type folder name because
                        // a/ we only accept an AndroidXmlEditor source and
                        // b/ aapt generates a compilation error for unknown folders.
                        IPath path = file.getFullPath();
                        // check if we are inside the project/res/* folder.
                        if (path.segmentCount() == 4) {
                            if (path.segment(1).equalsIgnoreCase(SdkConstants.FD_RESOURCES)) {


                                TextFileChange xmlChange = editXmlResourceFile(file);
                                if (xmlChange != null) {
                                    mChanges.add(xmlChange);
                                }
                            }
                        }
                    }
                }

                return false;

            } else if (resource instanceof IFolder) {
                return !SdkConstants.FD_GEN_SOURCES.equals(resource.getName());
            }

            return true;
        }
    }

    class ImportVisitor extends ASTVisitor {

        final AST mAst;
        final ASTRewrite mRewriter;

        ImportVisitor(AST ast) {
            mAst = ast;
            mRewriter = ASTRewrite.create(ast);
        }

        public TextEdit getTextEdit() {
            try {
                return this.mRewriter.rewriteAST();
            } catch (JavaModelException e) {
                Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e);
                AdtPlugin.getDefault().getLog().log(s);
            } catch (IllegalArgumentException e) {
                Status s = new Status(Status.ERROR, AdtPlugin.PLUGIN_ID, e.getMessage(), e);
                AdtPlugin.getDefault().getLog().log(s);
            }
            return null;
        }

        @Override
        public boolean visit(ImportDeclaration id) {

            Name importName = id.getName();
            if (importName.isQualifiedName()) {
                QualifiedName qualifiedImportName = (QualifiedName) importName;

                if (qualifiedImportName.getName().getIdentifier()
                        .equals(AdtConstants.FN_RESOURCE_BASE)) {
                    mRewriter.replace(qualifiedImportName.getQualifier(), mNewPackageName,
                            null);
                }
            }

            return true;
        }
    }
}
