﻿// DefaultScopeReport.cs
//
// Copyright 2010 Microsoft Corporation
//
// 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.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using NUglify.Helpers;
using NUglify.JavaScript;
using NUglify.JavaScript.Syntax;

namespace NUglify
{
    public sealed class DefaultScopeReport : IScopeReport
    {
        #region fields

        private TextWriter m_writer;
        private bool m_useReferenceCounts;

        #endregion

        #region IScopeReport Members

        public string Name
        {
            get { return "Default"; }
        }

        public void CreateReport(TextWriter writer, GlobalScope globalScope, bool useReferenceCounts)
        {
            if (writer != null && globalScope != null)
            {
                m_writer = writer;
                m_useReferenceCounts = useReferenceCounts;

                // output global scope report
                WriteScopeReport(globalScope);

                // generate a flat array of function scopes ordered by context line start
                var scopes = GetAllFunctionScopes(globalScope);

                // for each function scope, output a scope report
                foreach (ActivationObject scope in scopes)
                {
                    WriteScopeReport(scope);
                }

                // write the unreferenced global report
                WriteUnrefedReport(globalScope);
                m_writer.Flush();
                m_writer = null;
            }
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            if (m_writer != null)
            {
                m_writer.Flush();
                m_writer = null;
            }
        }

        #endregion

        #region private methods

        private ActivationObject[] GetAllFunctionScopes(GlobalScope globalScope)
        {
            // create a list to hold all the scopes
            List<ActivationObject> scopes = new List<ActivationObject>();

            // recursively add all the function scopes to the list
            AddScopes(scopes, globalScope);

            // sort the scopes by starting line (from the context)
            scopes.Sort(ScopeComparer.Instance);

            // return as an array
            return scopes.ToArray();
        }

        private void AddScopes(List<ActivationObject> list, ActivationObject parentScope)
        {
            // for each child scope...
            foreach (ActivationObject scope in parentScope.ChildScopes)
            {
                // add the scope to the list if it's not a globalscopes
                if (!(scope is GlobalScope))
                {
                    list.Add(scope);
                }

                // recurse...
                AddScopes(list, scope);
            }
        }

        private void WriteScopeReport(ActivationObject scope)
        {
            // output the function header
            if (scope is GlobalScope)
            {
                WriteProgress(NUglify.GlobalObjectsHeader);
            }
            else
            {
                ModuleScope moduleScope;
                if (scope is FunctionScope)
                {
                    WriteFunctionHeader(scope.Owner as FunctionObject, scope.IsKnownAtCompileTime, scope.UseStrict);
                }
                else if ((moduleScope = scope as ModuleScope) != null)
                {
                    WriteModuleHeader(moduleScope);
                }
                else
                {
                    string blockType;
                    switch (scope.ScopeType)
                    {
                        case ScopeType.Catch:
                            blockType = NUglify.BlockTypeCatch;
                            break;

                        case ScopeType.With:
                            blockType = NUglify.BlockTypeWith;
                            break;

                        case ScopeType.Class:
                            blockType = NUglify.BlockTypeClass.FormatInvariant(scope.ScopeName.IfNullOrWhiteSpace(NUglify.AnonymousName));
                            break;

                        case ScopeType.Block:
                        case ScopeType.Lexical:
                            blockType = NUglify.BlockTypeLexical;
                            break;

                        case ScopeType.Module:
                        case ScopeType.Function:
                        case ScopeType.Global:
                        case ScopeType.None:
                        default:
                            blockType = string.Empty;
                            System.Diagnostics.Debug.Fail("shouldn't get here");
                            break;
                    }

                    WriteBlockHeader(scope as BlockScope, blockType);
                }
            }

            // get all the fields in the scope
            List<JSVariableField> scopeFields = new List<JSVariableField>(scope.NameTable.Values);

            // sort the fields
            scopeFields.Sort(FieldComparer.Instance);

            // iterate over all the fields
            foreach (JSVariableField variableField in scopeFields)
            {
                // don't report placeholder fields or fields with the SpecialName attribute that aren't referenced
                if (!variableField.IsPlaceholder
                    && (variableField.Attributes != FieldAttributes.SpecialName || variableField.IsReferenced))
                {
                    WriteMemberReport(variableField);
                }
            }
        }

        private void WriteModuleHeader(ModuleScope moduleScope)
        {
            var blockType = NUglify.BlockTypeModule.FormatInvariant(moduleScope.ScopeName.IfNullOrWhiteSpace(NUglify.ModuleNameImplicit));
            string scopeFlags = null;
            var sb = StringBuilderPool.Acquire();
            try
            {
                if (!moduleScope.IsKnownAtCompileTime)
                {
                    sb.Append('[');
                    sb.Append(NUglify.NotKnown);
                    sb.Append(']');
                }

                if (moduleScope.UseStrict)
                {
                    sb.Append(NUglify.ScopeIsStrictFlag);
                }

                if (moduleScope.IsNotComplete)
                {
                    sb.Append(NUglify.ModuleIncompleteFlag);
                }

                scopeFlags = sb.ToString();
            }
            finally
            {
                sb.Release();
            }

            WriteProgress();
            WriteProgress(NUglify.BlockScopeHeader.FormatInvariant(
              blockType,
              moduleScope.Owner.Context.StartLineNumber,
              moduleScope.Owner.Context.StartColumn + 1,
              scopeFlags));

            if (moduleScope.HasDefaultExport)
            {
                // when there's a default export, we want to flag a line under the module
                // header that indicates that it's okay to bind to the default export.
                WriteProgress(NUglify.ModuleHasDefaultExport);
            }
        }

        private void WriteBlockHeader(BlockScope blockScope, string blockType)
        {
            string scopeFlags = null;
            var sb = StringBuilderPool.Acquire();
            try
            {
                if (!blockScope.IsKnownAtCompileTime)
                {
                    sb.Append('[');
                    sb.Append(NUglify.NotKnown);
                    sb.Append(']');
                }

                if (blockScope.UseStrict)
                {
                    sb.Append(NUglify.ScopeIsStrictFlag);
                }

                scopeFlags = sb.ToString();
            }
            finally
            {
                sb.Release();
            }

            WriteProgress();
            WriteProgress(NUglify.BlockScopeHeader.FormatInvariant(
              blockType,
              blockScope.Owner.Context.StartLineNumber,
              blockScope.Owner.Context.StartColumn + 1,
              scopeFlags));
        }

        //TYPE "NAME" - Starts at line LINE, col COLUMN STATUS [crunched to CRUNCH]
        //
        //TYPE: Function, Function getter, Function setter
        //STATUS: '', Unknown, Unreachable
        private void WriteFunctionHeader(FunctionObject funcObj, bool isKnown, bool useStrict)
        {
            // get the crunched value (if any)
            string crunched = string.Empty;
            var functionField = funcObj.Binding.IfNotNull(b => b.VariableField);
            if (functionField != null && functionField.CrunchedName != null)
            {
                crunched = NUglify.CrunchedTo.FormatInvariant(functionField.CrunchedName, functionField.RefCount);
            }

            // get the status if the function
            string status = null;
            var statusBuilder = StringBuilderPool.Acquire();
            try
            {
                if (!isKnown)
                {
                    statusBuilder.Append('[');
                    statusBuilder.Append(NUglify.NotKnown);
                }
                if (funcObj.EnclosingScope.Parent is GlobalScope)
                {
                    // global function.
                    // if this is a named function expression, we still want to know if it's
                    // referenced by anyone
                    if (funcObj.FunctionType == FunctionType.Expression
                        && funcObj.Binding != null
                        && !funcObj.Binding.Name.IsNullOrWhiteSpace())
                    {
                        // output a comma separator if not the first item, otherwise 
                        // open the square bracket
                        if (statusBuilder.Length > 0)
                        {
                            statusBuilder.Append(", ");
                        }
                        else
                        {
                            statusBuilder.Append('[');
                        }
                        statusBuilder.Append(NUglify.FunctionInfoReferences.FormatInvariant(
                            funcObj.Binding.VariableField.IfNotNull(v => v.RefCount)
                            ));
                    }
                }
                else if (!funcObj.IsReferenced && m_useReferenceCounts)
                {
                    // local function that isn't referenced -- unreachable!
                    // output a comma separator if not the first item, otherwise 
                    // open the square bracket
                    if (statusBuilder.Length > 0)
                    {
                        statusBuilder.Append(", ");
                    }
                    else
                    {
                        statusBuilder.Append('[');
                    }

                    statusBuilder.Append(NUglify.Unreachable);
                }

                if (statusBuilder.Length > 0)
                {
                    statusBuilder.Append(']');
                }

                if (useStrict)
                {
                    statusBuilder.Append(NUglify.ScopeIsStrictFlag);
                }

                status = statusBuilder.ToString();
            }
            finally
            {
                statusBuilder.Release();
            }

            string functionType;
            switch (funcObj.FunctionType)
            {
                case FunctionType.Getter:
                    functionType = NUglify.FunctionTypePropGet;
                    break;

                case FunctionType.Setter:
                    functionType = NUglify.FunctionTypePropSet;
                    break;

                case FunctionType.Expression:
                    functionType = NUglify.FunctionTypeExpression;
                    break;

                case FunctionType.ArrowFunction:
                    functionType = NUglify.FunctionTypeArrow;
                    break;

                case FunctionType.Method:
                    functionType = NUglify.FunctionTypeMethod;
                    break;

                default:
                    functionType = NUglify.FunctionTypeFunction;
                    break;
            }

            var functionName = funcObj.Binding.IfNotNull(b => b.Name);
            if (functionName.IsNullOrWhiteSpace())
            {
                functionName = !funcObj.NameGuess.IsNullOrWhiteSpace()
                    ? '"' + funcObj.NameGuess + '"'
                    : NUglify.AnonymousName;
            }

            // output
            WriteProgress();
            WriteProgress(NUglify.FunctionHeader.FormatInvariant(
                functionType,
                functionName,
                funcObj.Context.StartLineNumber,
                funcObj.Context.StartColumn + 1,
                status,
                crunched,
                funcObj.IsGenerator ? NUglify.FunctionTypeGenerator : string.Empty));
        }

        // NAME [SCOPE TYPE] [crunched to CRUNCH]
        //
        // SCOPE: global, local, outer, ''
        // TYPE: var, function, argument, arguments array, possibly undefined
        private void WriteMemberReport(JSVariableField variableField)
        {
            // don't report arguments fields that aren't referenced because
            // we add those fields to the function scopes automatically
            if (variableField.FieldType != FieldType.Arguments || variableField.RefCount > 0)
            {
                string scope = string.Empty;
                string type = string.Empty;
                string crunched = string.Empty;
                string name = variableField.Name;
                if (variableField.IsLiteral)
                {
                    name = variableField.FieldValue.ToString();
                }

                // calculate the crunched label
                if (variableField.CrunchedName != null)
                {
                    crunched = NUglify.CrunchedTo.FormatInvariant(variableField.CrunchedName, variableField.RefCount);
                }
                else
                {
                    crunched = NUglify.MemberInfoReferences.FormatInvariant(variableField.RefCount);
                }

                // get the field's default scope and type
                GetFieldScopeType(variableField, out scope, out type);
                if (variableField.FieldType == FieldType.WithField)
                {
                    // if the field is a with field, we won't be using the crunched field (since
                    // those fields can't be crunched), so let's overload it with what the field
                    // could POSSIBLY be if the with object doesn't have a property of that name
                    if (variableField.OuterField != null)
                    {
                        // make sure we get the OUTERMOST outer ield
                        var outerField = variableField.OuterField;
                        while (outerField.OuterField != null)
                        {
                            outerField = outerField.OuterField;
                        }

                        string outerScope;
                        string outerType;
                        GetFieldScopeType(outerField, out outerScope, out outerType);
                        if (!outerScope.IsNullOrWhiteSpace() || !outerType.IsNullOrWhiteSpace())
                        {
                            crunched = NUglify.MemberInfoWithPossibly.FormatInvariant(outerScope, outerType);
                        }
                    }
                    else
                    {
                        // no outer field. Then it must be a lexically-declared field inside the with-statement's
                        // block. These are tricky! At run-time if you try to declare a function, const, or let
                        // inside the with-scope, and the with-object already has a property with that name, the
                        // declaration will throw a run-time error.
                        crunched = variableField.IsFunction ? NUglify.MemberInfoWithLexFunc : NUglify.MemberInfoWithLexDecl;
                    }
                }

                var definedLocation = string.Empty;
                var definedContext = GetFirstDeclaration(variableField);
                if (definedContext != null)
                {
                    definedLocation = NUglify.MemberInfoDefinedLocation.FormatInvariant(definedContext.StartLineNumber, definedContext.StartColumn + 1);
                }

                // format the entire string
                WriteProgress(NUglify.MemberInfoFormat.FormatInvariant(
                    name,
                    scope,
                    type,
                    crunched,
                    definedLocation
                    ));
            }
        }

        private static SourceContext GetFirstDeclaration(JSVariableField variableField)
        {
            // only local fields that actually correspond to a declaration get the declaration
            // added to the declarations collection -- inner references don't get the declaration,
            // and neither do ghosted variables. So starting from this variable, walk up the 
            // outer-reference chain until we find on with at least one declaration. If we don't
            // find one, that's fine -- there probably isn't a declaration for it (predefined, for example).
            while (variableField != null && variableField.Declarations.Count == 0)
            {
                variableField = variableField.OuterField;
            }

            if (variableField != null)
            {
                foreach (var declaration in variableField.Declarations)
                {
                    // we only care about the FIRST declaration, so return the context of the name
                    return declaration.Context;
                }
            }

            // if we get here, there were no declarations
            return null;
        }

        private static void GetFieldScopeType(JSVariableField variableField, out string scope, out string type)
        {
            // default scope is blank
            scope = string.Empty;
            switch (variableField.FieldType)
            {
                case FieldType.Argument:
                    type = NUglify.MemberInfoTypeArgument;
                    if (variableField.IsOuterReference)
                    {
                        scope = NUglify.MemberInfoScopeOuter;
                    }
                    break;

                case FieldType.Arguments:
                    type = NUglify.MemberInfoTypeArguments;
                    if (variableField.IsOuterReference)
                    {
                        scope = NUglify.MemberInfoScopeOuter;
                    }
                    break;

                case FieldType.CatchError:
                    type = NUglify.MemberInfoTypeCatchEror;
                    if (variableField.IsOuterReference)
                    {
                        scope = NUglify.MemberInfoScopeOuter;
                    }
                    break;

                case FieldType.Super:
                    type = NUglify.MemberInfoTypeSuper;
                    if (variableField.IsOuterReference)
                    {
                        scope = NUglify.MemberInfoScopeOuter;
                    }
                    break;

                case FieldType.GhostCatch:
                case FieldType.GhostFunction:
                default:
                    // ghost fields -- ignore
                    type = string.Empty;
                    break;

                case FieldType.Global:
                case FieldType.UndefinedGlobal:
                    if ((variableField.Attributes & FieldAttributes.RTSpecialName) == FieldAttributes.RTSpecialName)
                    {
                        // this is a special "global." It might not be a global, but something referenced
                        // in a with scope somewhere down the line.
                        type = NUglify.MemberInfoPossiblyUndefined;
                    }
                    else if (variableField.FieldValue is FunctionObject && variableField.GhostedField != null)
                    {
                        type = NUglify.MemberInfoFunctionExpression;
                    }
                    else
                    {
                        goto case FieldType.Local;
                    }
                    break;

                case FieldType.Local:
                    // type string
                    if (variableField.FieldValue is FunctionObject)
                    {
                        if (variableField.GhostedField == null)
                        {
                            type = NUglify.MemberInfoFunction;
                        }
                        else
                        {
                            type = NUglify.MemberInfoFunctionExpression;
                        }
                    }
                    else if (variableField.FieldValue is ClassNode)
                    {
                        type = NUglify.MemberInfoClass;
                    }
                    else if (variableField.IsLiteral)
                    {
                        type = NUglify.MemberInfoLiteral;
                    }
                    else if (variableField.InitializationOnly)
                    {
                        type = NUglify.MemberInfoConst;
                    }
                    else
                    {
                        type = NUglify.MemberInfoVar;
                    }

                    // scope string
                    if (variableField.IsOuterReference)
                    {
                        scope = NUglify.MemberInfoScopeOuter;
                    }
                    else if (variableField.FieldType == FieldType.Global || variableField.FieldType == FieldType.UndefinedGlobal)
                    {
                        scope = NUglify.MemberInfoScopeGlobal;
                    }
                    else
                    {
                        scope = NUglify.MemberInfoScopeLocal;
                    }
                    break;

                case FieldType.Predefined:
                    scope = NUglify.MemberInfoScopeGlobalObject;
                    type = variableField.IsFunction
                        ? NUglify.MemberInfoBuiltInMethod
                        : NUglify.MemberInfoBuiltInProperty;
                    break;

                case FieldType.WithField:
                    type = NUglify.MemberInfoWithField;
                    if (variableField.IsOuterReference)
                    {
                        scope = NUglify.MemberInfoScopeOuter;
                    }
                    break;
            }

            if (variableField.IsExported)
            {
                scope = NUglify.MemberInfoScopeExported + scope;
            }
        }

        private void WriteUnrefedReport(GlobalScope globalScope)
        {
            if (globalScope.UndefinedReferences != null && globalScope.UndefinedReferences.Count > 0)
            {
                // sort the undefined reference exceptions
                var undefinedList = new List<UndefinedReference>(globalScope.UndefinedReferences);
                undefinedList.Sort((left, right) =>
                    {
                        // first do the right thing if one or both are null
                        if (left == null && right == null)
                        {
                            // both null -- equal
                            return 0;
                        }

                        if (left == null)
                        {
                            // left is null, right is not -- left is less
                            return -1;
                        }

                        if (right == null)
                        {
                            // left is not null, right is -- left is more
                            return 1;
                        }

                        // neither are null
                        int comparison = string.Compare(left.ToString(), right.ToString(), StringComparison.OrdinalIgnoreCase);
                        if (comparison == 0)
                        {
                            comparison = left.Line - right.Line;
                            if (comparison == 0)
                            {
                                comparison = left.Column - right.Column;
                            }
                        }

                        return comparison;
                    });

                // write the report
                WriteProgress();
                WriteProgress(NUglify.UndefinedGlobalHeader);
                foreach (UndefinedReference ex in undefinedList)
                {
                    WriteProgress(NUglify.UndefinedInfo.FormatInvariant(
                      ex.Name,
                      ex.Line,
                      ex.Column,
                      ex.ReferenceType.ToString()
                      ));
                }
            }
        }

        #endregion

        #region output methods

        private void WriteProgress()
        {
            WriteProgress(string.Empty);
        }

        private void WriteProgress(string format, params object[] args)
        {
            try
            {
                m_writer.WriteLine(format, args);
            }
            catch (FormatException)
            {
                m_writer.WriteLine(format);
            }
        }

        #endregion

        #region Comparer classes

        private class ScopeComparer : IComparer<ActivationObject>
        {
            // singleton instance
            public static readonly IComparer<ActivationObject> Instance = new ScopeComparer();

            // private constructor -- use singleton
            private ScopeComparer() { }

            #region IComparer<ActivationObject> Members

            public int Compare(ActivationObject left, ActivationObject right)
            {
                int comparison = 0;
                SourceContext leftContext = GetContext(left);
                SourceContext rightContext = GetContext(right);
                if (leftContext == null)
                {
                    // if they're both null, return 0 (equal)
                    // otherwise just the left is null, so we want it at the end, so
                    // return 1 to indicate that it goes after the right context
                    return (rightContext == null ? 0 : 1);
                }
                else if (rightContext == null)
                {
                    // return -1 to indicate that the right context (null) goes after the left
                    return -1;
                }

                // compare their start lines
                comparison = leftContext.StartLineNumber - rightContext.StartLineNumber;
                if (comparison == 0)
                {
                    comparison = leftContext.StartColumn - rightContext.StartColumn;
                    if (comparison == 0)
                    {
                        // starts at the same place. It could be the named-function expression
                        // scopes - there's one just for the name that contains the real function
                        // scope so the function expression name doesn't pollute the containing scope.
                        // that one should go before the regular one
                        if (IsFunctionExpressionName(left))
                        {
                            comparison = -1;
                        }
                        else if (IsFunctionExpressionName(right))
                        {
                            comparison = 1;
                        }
                    }
                }
                return comparison;
            }

            private static bool IsFunctionExpressionName(ActivationObject scope)
            {
                // for this to be a function expression name scope:
                // 1. the owner must not be null (well, it shouldn't ever be null)
                // 2. the owner must have its own scope (all function objects should point to their own scope)
                // 3. and this scope's owner's scope must point to a scope different than the current scope.
                return scope.Owner.IfNotNull(o => o.HasOwnScope && o.EnclosingScope != scope);
            }

            private static SourceContext GetContext(ActivationObject obj)
            {
                // return the owner context, or null
                return obj.IfNotNull(s => s.Owner.Context);
            }

            #endregion
        }

        private class FieldComparer : IComparer<JSVariableField>
        {
            // singleton instance
            public static readonly IComparer<JSVariableField> Instance = new FieldComparer();

            // private constructor -- use singleton
            private FieldComparer() { }

            #region IComparer<JSVariableField> Members

            /// <summary>
            /// Local Argument fields first
            /// Local Arguments object
            /// Local Fields defined
            /// Local Functions defined
            /// Outer Argument fields
            /// Outer Arguments object
            /// Outer fields referenced
            /// Outer Functions referenced
            /// Global fields referenced
            /// Global functions referenced
            /// </summary>
            /// <param name="x">left-hand object</param>
            /// <param name="y">right-hand object</param>
            /// <returns>&gt;0 left before right, &lt;0 right before left</returns>
            public int Compare(JSVariableField left, JSVariableField right)
            {
                int comparison = 0;
                if (left != null && right != null)
                {
                    // compare type class
                    comparison = GetOrderIndex(left) - GetOrderIndex(right);
                    if (comparison == 0)
                    {
                        // sort alphabetically
                        comparison = string.Compare(
                          left.Name,
                          right.Name,
                          StringComparison.OrdinalIgnoreCase
                          );
                    }
                }
                return comparison;
            }

            #endregion

            private static FieldOrder GetOrderIndex(JSVariableField obj)
            {
                if (obj.FieldType == FieldType.Argument || obj.FieldType == FieldType.CatchError)
                {
                    return obj.IsOuterReference ? FieldOrder.OuterArgumentReferenced : FieldOrder.LocalArgument;
                }
                if (obj.FieldType == FieldType.Arguments)
                {
                    return obj.IsOuterReference ? FieldOrder.OuterArgumentsObject : FieldOrder.LocalArgumentsObject;
                }

                if (obj.FieldType == FieldType.Super)
                {
                    return FieldOrder.LocalSuperReference;
                }

                if (obj.IsExported)
                {
                    if (obj.OuterField == null)
                    {
                        return FieldOrder.ExportedDeclaration;
                    }

                    return FieldOrder.ExportedReferences;
                }

                if (obj.FieldType == FieldType.Global)
                {
                    return (
                      obj.FieldValue is FunctionObject
                      ? FieldOrder.GlobalFunctionReferenced
                      : FieldOrder.GlobalFieldReferenced
                      );
                }

                if (obj.FieldType == FieldType.Predefined)
                {
                    return FieldOrder.Predefined;
                }

                if (obj.FieldValue is FunctionObject)
                {
                    // function
                    return obj.IsOuterReference
                        ? FieldOrder.OuterFunctionReferenced
                        : FieldOrder.LocalFunctionDefined;
                }

                // field
                return obj.IsOuterReference
                    ? FieldOrder.OuterFieldReferenced
                    : FieldOrder.LocalFieldDefined;
            }

            private enum FieldOrder : int
            {
                LocalArgument = 0,
                LocalArgumentsObject,
                LocalSuperReference,
                ExportedDeclaration,
                ExportedReferences,
                LocalFieldDefined,
                LocalFunctionDefined,
                OuterArgumentReferenced,
                OuterArgumentsObject,
                OuterFieldReferenced,
                OuterFunctionReferenced,
                GlobalFieldReferenced,
                GlobalFunctionReferenced,
                Predefined,
                Other
            }
        }

        #endregion
    }
}
