/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Dynamic;
using Microsoft.Scripting.Runtime;
using Microsoft.Scripting.Utils;
using System.Text;

namespace Microsoft.Scripting {
    [DebuggerDisplay("{_path ?? \"<anonymous>\"}")]
    public sealed class SourceUnit {
        private readonly SourceCodeKind _kind;
        private readonly string _path;
        private readonly LanguageContext _language;
        private readonly TextContentProvider _contentProvider;

        // SourceUnit is serializable => updated parse result is transmitted
        // back to the host unless the unit is passed by-ref
        private ScriptCodeParseResult? _parseResult;
        private KeyValuePair<int, int>[] _lineMap;

        /// <summary>
        /// Identification of the source unit. Assigned by the host. 
        /// The format and semantics is host dependent (could be a path on file system or URL).
        /// Empty string for anonymous source units.
        /// </summary>
        public string Path {
            get { return _path; }
        }

        public bool HasPath {
            get { return _path != null; }
        }

        public SourceCodeKind Kind {
            get { return _kind; }
        }

        public SymbolDocumentInfo Document {
            get {
                // _path is valid to be null. In that case we cannot create a valid SymbolDocumentInfo.
                return _path == null ? null : Expression.SymbolDocument(_path, _language.LanguageGuid, _language.VendorGuid);
            }
        }

        /// <summary>
        /// LanguageContext of the language of the unit.
        /// </summary>
        public LanguageContext LanguageContext {
            get { return _language; }
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public ScriptCodeParseResult GetCodeProperties() {
            return GetCodeProperties(_language.GetCompilerOptions());
        }

        public ScriptCodeParseResult GetCodeProperties(CompilerOptions options) {
            ContractUtils.RequiresNotNull(options, "options");

            _language.CompileSourceCode(this, options, ErrorSink.Null);
            return _parseResult ?? ScriptCodeParseResult.Complete;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")] // TODO: fix
        public ScriptCodeParseResult? CodeProperties {
            get { return _parseResult; }
            set { _parseResult = value; }
        }

        public SourceUnit(LanguageContext context, TextContentProvider contentProvider, string path, SourceCodeKind kind) {
            Assert.NotNull(context, contentProvider);
            Debug.Assert(path == null || path.Length > 0);
            Debug.Assert(context.CanCreateSourceCode);

            _language = context;
            _contentProvider = contentProvider;
            _kind = kind;
            _path = path;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public SourceCodeReader GetReader() {
            return _contentProvider.GetReader();
        }

        /// <summary>
        /// Reads specified range of lines (or less) from the source unit. 
        /// Line numbers starts with 1.
        /// </summary>
        public string[] GetCodeLines(int start, int count) {
            ContractUtils.Requires(start > 0, "start");
            ContractUtils.Requires(count > 0, "count");

            List<string> result = new List<string>(count);

            using (SourceCodeReader reader = GetReader()) {
                reader.SeekLine(start);
                while (count > 0) {
                    string line = reader.ReadLine();
                    if (line == null) break;
                    result.Add(line);
                    count--;
                }
            }

            return result.ToArray();
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public string GetCodeLine(int line) {
            string[] lines = GetCodeLines(line, 1);
            return (lines.Length > 0) ? lines[0] : null;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public string GetCode() {
            using (SourceCodeReader reader = GetReader()) {
                return reader.ReadToEnd();
            }
        }

        #region Line/File mapping

        public SourceLocation MakeLocation(int index, int line, int column) {
            return new SourceLocation(index, MapLine(line), column);
        }
        public SourceLocation MakeLocation(SourceLocation loc) {
            return new SourceLocation(loc.Index, MapLine(loc.Line), loc.Column);
        }

        public int MapLine(int line) {
            if (_lineMap != null) {
                int match = BinarySearch(_lineMap, line);
                int delta = line - _lineMap[match].Key;
                line = _lineMap[match].Value + delta;
                if (line < 1) {
                    line = 1; // this is the minimum value
                }
            }

            return line;
        }

        private static int BinarySearch<T>(KeyValuePair<int, T>[] array, int line) {
            int match = Array.BinarySearch(array, new KeyValuePair<int, T>(line, default(T)), new KeyComparer<T>());
            if (match < 0) {
                // If we couldn't find an exact match for this line number, get the nearest
                // matching line number less than this one
                match = ~match - 1;

                // If our index = -1, it means that this line is before any line numbers that
                // we know about. If that's the case, use the first entry in the list
                if (match == -1) {
                    match = 0;
                }
            }
            return match;
        }


        private class KeyComparer<T1> : IComparer<KeyValuePair<int, T1>> {
            public int Compare(KeyValuePair<int, T1> x, KeyValuePair<int, T1> y) {
                return x.Key - y.Key;
            }
        }

        #endregion

        #region Parsing, Compilation, Execution

        public bool EmitDebugSymbols {
            get {
                return HasPath && LanguageContext.DomainManager.Configuration.DebugMode;
            }
        }

        public ScriptCode Compile() {
            return Compile(ErrorSink.Default);
        }

        public ScriptCode Compile(ErrorSink errorSink) {
            return Compile(_language.GetCompilerOptions(), errorSink);
        }

        /// <summary>
        /// Errors are reported to the specified sink. 
        /// Returns <c>null</c> if the parser cannot compile the code due to error(s).
        /// </summary>
        public ScriptCode Compile(CompilerOptions options, ErrorSink errorSink) {
            ContractUtils.RequiresNotNull(errorSink, "errorSink");
            ContractUtils.RequiresNotNull(options, "options");

            return _language.CompileSourceCode(this, options, errorSink);
        }

        /// <summary>
        /// Executes against a specified scope.
        /// </summary>
        public object Execute(Scope scope) {
            return Execute(scope, ErrorSink.Default);
        }

        /// <summary>
        /// Executes against a specified scope and reports errors to the given error sink.
        /// </summary>
        public object Execute(Scope scope, ErrorSink errorSink) {
            ContractUtils.RequiresNotNull(scope, "scope");

            ScriptCode compiledCode = Compile(_language.GetCompilerOptions(scope), errorSink);

            if (compiledCode == null) {
                throw new SyntaxErrorException();
            }

            return compiledCode.Run(scope);
        }

        /// <summary>
        /// Executes in a new scope created by the language.
        /// </summary>
        public object Execute() {
            return Compile().Run();
        }

        /// <summary>
        /// Executes in a new scope created by the language.
        /// </summary>
        public object Execute(ErrorSink errorSink) {
            return Compile(errorSink).Run();
        }

        /// <summary>
        /// Executes in a new scope created by the language.
        /// </summary>
        public object Execute(CompilerOptions options, ErrorSink errorSink) {
            return Compile(options, errorSink).Run();
        }

        public int ExecuteProgram() {
            return _language.ExecuteProgram(this);
        }

        #endregion

        public void SetLineMapping(KeyValuePair<int, int>[] lineMap) {
            _lineMap = (lineMap == null || lineMap.Length == 0) ? null : lineMap;
        }
    }
}
