/* 
(c) SIB Visions 2012 
http://www.sibvisions.com
*/
public class SimpleSourceFile 
{
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Class members
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /** the source parser. */
    private ASTParser parser;
    /** the parsed tokens of the source file. */
    private CompilationUnit cuSource;
    /** the original document. */
    private Document docOrig;
    /** the class type info. */
    private TypeDeclaration tdClass;
    /** the javadoc of the class. */
    private JavadocInfo jdoc;
    /** the full qualified name of the class. */
    private String sClassName;
    /** the full qualified name of the super class after loading. */
    private String sLoadedSuperClass;
    /** the full qualified name of the parent class. */
    private String sSuperClass;
    /** whether the source is changed. */
    private boolean bChanged = false;
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Initialization
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /**
     * Creates a new instance of <code>SimpleSourceFile</code> without source
     * information.
     */
    public SimpleSourceFile() {
        load("");
    }
    /**
     * Creates a new instance of <code>SimpleSourceFile</code> with a given 
     * source file.
     *  
     * @param pFile the java source file
     * @throws IOException if an error occurs during reading the file
     */
    public SimpleSourceFile(File pFile) throws IOException {
        if (pFile.exists()) {
            JavaSource source = JavaSource.get(pFile.getAbsolutePath());
            if (source != null) {
                synchronized(source) {
                    load(new String(FileUtil.getContent(pFile)));
                }
            } else {
                load(new String(FileUtil.getContent(pFile)));
            }
        } else {
            load("");
        }
    }
    /**
     * Creates a new instance of <code>SimpleSourceFile</code> with a given 
     * source stream. The stream is closed after reading
     *  
     * @param pStream the java source stream
     * @throws IOException if an error occurs during reading the stream
     */
    public SimpleSourceFile(InputStream pStream) throws IOException {
        this(pStream, true);
    }
    /**
     * Creates a new instance of <code>SimpleSourceFile</code> with a given 
     * source stream.
     *  
     * @param pStream the java source stream
     * @param pAutoClose whether the stream should be closed after reading
     * @throws IOException if an error occurs during reading the stream
     */
    public SimpleSourceFile(InputStream pStream, boolean pAutoClose) throws IOException {
        load(new String(FileUtil.getContent(pStream, pAutoClose)));
    }
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // User-defined methods
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    /**
     * Loads the properties from the source file.
     * 
     * @param pSource the java source
     */@SuppressWarnings({
        "unchecked", "rawtypes"
    })
    private void load(String pSource) {
        jdoc = new JavadocInfo();
        if (parser == null) {
            parser = ASTUtil.createParser();
        }
        parser.setSource(pSource.toCharArray());
        cuSource = (CompilationUnit) parser.createAST(null);
        if (docOrig != null) {
            docOrig.set(pSource);
        } else {
            docOrig = new Document(pSource);
        }
        //we support only one class per file
        List types = cuSource.types();
        if (types.size() > 0 && types.get(0) instanceof TypeDeclaration) {
            tdClass = (TypeDeclaration) types.get(0);
            String sPackage;
            //we have to use the current package, if set
            if (cuSource.getPackage() != null) {
                sPackage = cuSource.getPackage().getName().getFullyQualifiedName() + ".";
            } else {
                sPackage = "";
            }
            sClassName = sPackage + tdClass.getName().getFullyQualifiedName();
            if (tdClass.getSuperclassType() != null) {
                String sSuperName = ((SimpleType)(tdClass.getSuperclassType())).getName().getFullyQualifiedName();
                if (sSuperName.indexOf('.') > 0) {
                    sSuperClass = sSuperName;
                } else {
                    //try to detect the full qualified name of the super class through the imports
                    List < ImportDeclaration > liImports = cuSource.imports();
                    boolean bFound = false;
                    if (liImports != null) {
                        String sTempName = "." + sSuperName;
                        String sImport;
                        for (int i = 0, anz = liImports.size(); !bFound && i < anz; i++) {
                            sImport = liImports.get(i).getName().getFullyQualifiedName();
                            if (sImport.endsWith(sTempName)) {
                                bFound = true;
                                sSuperClass = liImports.get(i).getName().getFullyQualifiedName();
                            }
                        }
                    }
                    if (!bFound) {
                        //we use the current package
                        sSuperClass = sPackage + sSuperName;
                    }
                }
                //cache for optimizing imports in apply
                sLoadedSuperClass = sSuperClass;
            }
            jdoc = new JavadocInfo(tdClass.getJavadoc(), pSource);
        } else {
            tdClass = null;
        }
        //currently loaded file can not be changed!
        bChanged = false;
    }
    /**
     * Sets the comment for the class.
     * 
     * @param pText the comment
     */
    public void setClassComment(String pText) {
        bChanged |= !CommonUtil.equals(pText, jdoc.getDescription());
        jdoc.setDescription(pText);
    }
    /**
     * Gets the comment for the class.
     * 
     * @return the comment
     */
    public String getClassComment() {
        return jdoc.getDescription();
    }
    /**
     * Sets the full qualified name of the class.
     * 
     * @param pName the class name
     */
    public void setClassName(String pName) {
        if (pName == null) {
            throw new IllegalArgumentException("It is not allowed to remove the class definition (class name = null)!");
        }
        bChanged |= !CommonUtil.equals(pName, sClassName);
        sClassName = pName;
    }
    /**
     * Gets the full qualified name of the class.
     * 
     * @return the class name
     */
    public String getClassName() {
        return sClassName;
    }
    /**
     * Sets the full qualified class name of the parent class.
     * 
     * @param pSuperClass the full qualified class name of the parent class
     */
    public void setSuperClass(String pSuperClass) {
        bChanged |= !CommonUtil.equals(pSuperClass, sSuperClass);
        sSuperClass = pSuperClass;
    }
    /**
     * Gets the full qualified class name of the parent class.
     * 
     * @return the class name
     */
    public String getSuperClass() {
        return sSuperClass;
    }
    /**
     * Replaces the relevant information with the specific values
     * and saves the source file.
     * 
     * @throws Exception if the source modification fails
     */@SuppressWarnings("unchecked")
    protected void apply() throws Exception {
        if (!bChanged) {
            return;
        }
        AST ast = cuSource.getAST();
        ASTRewrite arw = ASTRewrite.create(ast);
        //----------------------------------------------------
        // Change Package, Classname and Constructor
        //----------------------------------------------------
        String sCurrentPackage;
        //set package
        String[] sClassInfo = ClassUtil.splitName(sClassName);
        if (sClassInfo[0] != null) {
            if (cuSource.getPackage() != null) {
                arw.set(cuSource.getPackage(), PackageDeclaration.NAME_PROPERTY, ast.newName(sClassInfo[0]), null);
                sCurrentPackage = sClassInfo[0];
            } else {
                PackageDeclaration pdcl = ast.newPackageDeclaration();
                pdcl.setName(ast.newName(sClassInfo[0]));
                arw.set(cuSource, CompilationUnit.PACKAGE_PROPERTY, pdcl, null);
                sCurrentPackage = sClassInfo[0];
            }
        } else {
            arw.set(cuSource, CompilationUnit.PACKAGE_PROPERTY, null, null);
            sCurrentPackage = null;
        }
        if (tdClass == null) {
            //new class
            tdClass = ast.newTypeDeclaration();
            tdClass.setName(ast.newSimpleName(sClassInfo[1]));
            ListRewrite lrwImp = arw.getListRewrite(cuSource, CompilationUnit.TYPES_PROPERTY);
            lrwImp.insertFirst(tdClass, null);
        } else {
            //change class name
            SimpleName snClass = ast.newSimpleName(sClassInfo[1]);
            arw.set(tdClass, TypeDeclaration.NAME_PROPERTY, snClass, null);
            //chage constructors
            MethodDeclaration[] methods = tdClass.getMethods();
            if (methods != null) {
                for (MethodDeclaration method: methods) {
                    if (method.isConstructor()) {
                        arw.set(method, MethodDeclaration.NAME_PROPERTY, snClass, null);
                    }
                }
            }
        }
        //change Superclass
        List < ImportDeclaration > liImports = cuSource.imports();
        //remove "old" import
        if (sLoadedSuperClass != null) {
            if (liImports != null) {
                boolean bFound = false;
                for (int i = liImports.size() - 1; !bFound && i >= 0; i--) {
                    if (sLoadedSuperClass.equals(liImports.get(i).getName().getFullyQualifiedName())) {
                        bFound = true;
                        arw.remove(liImports.get(i), null);
                        liImports.remove(i);
                    }
                }
            }
        }
        if (sSuperClass == null) {
            arw.set(tdClass, TypeDeclaration.SUPERCLASS_TYPE_PROPERTY, null, null);
        } else {
            //check new super class import
            String[] sSuperClassInfo = ClassUtil.splitName(sSuperClass);
            if (sSuperClassInfo[0] != null) {
                boolean bFound = false;
                if (sCurrentPackage != null) {
                    bFound = sSuperClassInfo[0].equals(sCurrentPackage);
                }
                if (!bFound) {
                    if (liImports != null) {
                        for (int i = 0, anz = liImports.size(); !bFound && i < anz; i++) {
                            bFound = sSuperClass.equals(liImports.get(i).getName().getFullyQualifiedName());
                        }
                    }
                }
                //class not already importet -> add import
                if (!bFound) {
                    ImportDeclaration idSuper = ast.newImportDeclaration();
                    idSuper.setName(ast.newName(sSuperClass));
                    ListRewrite lrwImp = arw.getListRewrite(cuSource, CompilationUnit.IMPORTS_PROPERTY);
                    lrwImp.insertLast(idSuper, null);
                }
            }
            arw.set(tdClass, TypeDeclaration.SUPERCLASS_TYPE_PROPERTY, ast.newSimpleType(ast.newName(sSuperClassInfo[1])), null);
        }
        //----------------------------------------------------
        // Change Javadoc of the class
        //----------------------------------------------------
        if ("".equals(jdoc.toString())) {
            //no javadoc
            arw.set(tdClass, TypeDeclaration.JAVADOC_PROPERTY, null, null);
        } else {
            arw.set(tdClass, TypeDeclaration.JAVADOC_PROPERTY, jdoc.createJavadoc(tdClass.getAST()), null);
        }
        //----------------------------------------------------
        // Modify AST
        //----------------------------------------------------
        TextEdit edit = arw.rewriteAST(docOrig, null);
        edit.apply(docOrig);
        ASTUtil.applyFormat(docOrig);
        //simple reparse (important for the comments)
        parser.setSource(docOrig.get().toCharArray());
        cuSource = (CompilationUnit) parser.createAST(null);
        //----------------------------------------------------
        // Modify comments
        //----------------------------------------------------
        //Class comment after the last bracket
        int iCorr = 0;
        List < Comment > liComments = cuSource.getCommentList();
        if (liComments != null) {
            String sOldName = tdClass.getName().getFullyQualifiedName();
            String sText;
            for (Comment com: liComments) {
                sText = docOrig.get(com.getStartPosition() - iCorr, com.getLength());
                int iPos = 0;
                int iClassLen = sClassInfo[1].length();
                int iLenDiff = sOldName.length() - iClassLen;
                int iNextPos;
                CharacterType ctypePrev;
                CharacterType ctypeNext;
                while ((iPos = sText.indexOf(sOldName, iPos)) >= 0) {
                    if (iPos > 0) {
                        ctypePrev = StringUtil.getCharacterType("" + sText.charAt(iPos - 1));
                    } else {
                        ctypePrev = CharacterType.OnlySpecial;
                    }
                    iNextPos = iPos + sOldName.length();
                    if (iNextPos <= sText.length() - 1) {
                        ctypeNext = StringUtil.getCharacterType("" + sText.charAt(iNextPos));
                    } else {
                        ctypeNext = CharacterType.OnlySpecial;
                    }
                    //don't replace word parts e.g. ContactsWorkScreen should not be replaced with Contacts
                    if ((ctypePrev == CharacterType.OnlySpecial || ctypePrev == CharacterType.OnlyWhitespace) && (ctypeNext == CharacterType.OnlySpecial || ctypeNext == CharacterType.OnlyWhitespace)) {
                        docOrig.replace(com.getStartPosition() + iPos - iCorr, sOldName.length(), sClassInfo[1]);
                        iCorr += iLenDiff;
                        iPos += iClassLen;
                    } else {
                        iPos = iNextPos;
                    }
                }
            }
        }
        //----------------------------------------------------
        // Update
        //----------------------------------------------------
        load(docOrig.get());
        bChanged = false;
    }
    /**
     * Stores the changed source into the given file.
     * 
     * @param pFile the target file for the source
     * @throws Exception if an error occurs during saving
     */
    public void saveAs(File pFile) throws Exception {
        apply();
        JavaSource source = JavaSource.get(pFile.getAbsolutePath());
        if (source != null) {
            synchronized(source) {
                FileUtil.save(pFile, docOrig.get().getBytes());
                source.reload();
            }
        } else {
            FileUtil.save(pFile, docOrig.get().getBytes());
        }
    }
    /**
     * Stores the changed source into the given stream.
     * 
     * @param pStream the target stream for the source
     * @throws Exception if an error occurs during saving
     */
    public void saveAs(OutputStream pStream) throws Exception {
        apply();
        pStream.write(docOrig.get().getBytes());
        pStream.flush();
    }
    /**
     * Returns the modified source code.
     *  
     * @return the modified source code
     * @throws Exception if the source modification fails
     */
    protected String getSource() throws Exception {
        apply();
        return docOrig.get();
    }
} // SimpleSourceFile