using J2N.Collections.Generic.Extensions;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;

namespace Lucene.Net.Index
{
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF licenses this file to You 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 Codec = Lucene.Net.Codecs.Codec;
    using Directory = Lucene.Net.Store.Directory;
    using Lucene3xSegmentInfoFormat = Lucene.Net.Codecs.Lucene3x.Lucene3xSegmentInfoFormat;
    using TrackingDirectoryWrapper = Lucene.Net.Store.TrackingDirectoryWrapper;

    /// <summary>
    /// Information about a segment such as it's name, directory, and files related
    /// to the segment.
    /// <para/>
    /// @lucene.experimental
    /// </summary>
    public sealed class SegmentInfo
    {
        // TODO: remove these from this class, for now this is the representation
        /// <summary>
        /// Used by some member fields to mean not present (e.g.,
        /// norms, deletions).
        /// </summary>
        public static readonly int NO = -1; // e.g. no norms; no deletes;

        /// <summary>
        /// Used by some member fields to mean present (e.g.,
        /// norms, deletions).
        /// </summary>
        public static readonly int YES = 1; // e.g. have norms; have deletes;

        /// <summary>
        /// Unique segment name in the directory. </summary>
        public string Name { get; private set; }

        private int docCount; // number of docs in seg

        /// <summary>
        /// Where this segment resides. </summary>
        public Directory Dir { get; private set; }

        private bool isCompoundFile;

        private Codec codec;

        private IDictionary<string, string> diagnostics;

        [Obsolete("not used anymore")]
        private IDictionary<string, string> attributes;

        // Tracks the Lucene version this segment was created with, since 3.1. Null
        // indicates an older than 3.0 index, and it's used to detect a too old index.
        // The format expected is "x.y" - "2.x" for pre-3.0 indexes (or null), and
        // specific versions afterwards ("3.0", "3.1" etc.).
        // see Constants.LUCENE_MAIN_VERSION.
        private string version;

        /// <summary>
        /// Gets or Sets diagnostics saved into the segment when it was written.
        /// </summary>
        public IDictionary<string, string> Diagnostics
        {
            get => diagnostics;
            set => this.diagnostics = value;
        }

        /// <summary>
        /// Construct a new complete <see cref="SegmentInfo"/> instance from input.
        /// <para>Note: this is public only to allow access from
        /// the codecs package.</para>
        /// </summary>
        public SegmentInfo(Directory dir, string version, string name, int docCount, bool isCompoundFile, Codec codec, IDictionary<string, string> diagnostics)
            : this(dir, version, name, docCount, isCompoundFile, codec, diagnostics, null)
        {
        }

        /// <summary>
        /// Construct a new complete <see cref="SegmentInfo"/> instance from input.
        /// <para>Note: this is public only to allow access from
        /// the codecs package.</para>
        /// </summary>
        public SegmentInfo(Directory dir, string version, string name, int docCount, bool isCompoundFile, Codec codec, IDictionary<string, string> diagnostics, IDictionary<string, string> attributes)
        {
            Debug.Assert(!(dir is TrackingDirectoryWrapper));
            this.Dir = dir;
            this.version = version;
            this.Name = name;
            this.docCount = docCount;
            this.isCompoundFile = isCompoundFile;
            this.codec = codec;
            this.diagnostics = diagnostics;
#pragma warning disable 612, 618
            this.attributes = attributes;
#pragma warning restore 612, 618
        }

        [Obsolete("separate norms are not supported in >= 4.0")]
        internal bool HasSeparateNorms => GetAttribute(Lucene3xSegmentInfoFormat.NORMGEN_KEY) != null;

        /// <summary>
        /// Gets or Sets whether this segment is stored as a compound file.
        /// <c>true</c> if this is a compound file;
        /// else, <c>false</c>
        /// </summary>
        public bool UseCompoundFile
        {
            get => isCompoundFile;
            set => this.isCompoundFile = value;
        }

        /// <summary>
        /// Gets or Sets <see cref="Codecs.Codec"/> that wrote this segment.
        /// Setter can only be called once. </summary>
        public Codec Codec
        {
            get => codec;
            set
            {
                Debug.Assert(this.codec == null);
                if (value == null)
                {
                    throw new ArgumentException("codec must be non-null");
                }
                this.codec = value;
            }
        }

        /// <summary>
        /// Returns number of documents in this segment (deletions
        /// are not taken into account).
        /// </summary>
        public int DocCount
        {
            get
            {
                if (this.docCount == -1)
                {
                    throw new InvalidOperationException("docCount isn't set yet");
                }
                return docCount;
            }
            internal set // NOTE: leave package private
            {
                if (this.docCount != -1)
                {
                    throw new InvalidOperationException("docCount was already set");
                }
                this.docCount = value;
            }
        }

        /// <summary>
        /// Return all files referenced by this <see cref="SegmentInfo"/>. </summary>
        public ISet<string> GetFiles()
        {
            if (setFiles == null)
            {
                throw new InvalidOperationException("files were not computed yet");
            }
            return setFiles.AsReadOnly();
        }

        public override string ToString()
        {
            return ToString(Dir, 0);
        }

        /// <summary>
        /// Used for debugging.  Format may suddenly change.
        ///
        /// <para>Current format looks like
        /// <c>_a(3.1):c45/4</c>, which means the segment's
        /// name is <c>_a</c>; it was created with Lucene 3.1 (or
        /// '?' if it's unknown); it's using compound file
        /// format (would be <c>C</c> if not compound); it
        /// has 45 documents; it has 4 deletions (this part is
        /// left off when there are no deletions).</para>
        /// </summary>
        public string ToString(Directory dir, int delCount)
        {
            StringBuilder s = new StringBuilder();
            s.Append(Name).Append('(').Append(version == null ? "?" : version).Append(')').Append(':');
            char cfs = UseCompoundFile ? 'c' : 'C';
            s.Append(cfs);

            if (this.Dir != dir)
            {
                s.Append('x');
            }
            s.Append(docCount);

            if (delCount != 0)
            {
                s.Append('/').Append(delCount);
            }

            // TODO: we could append toString of attributes() here?

            return s.ToString();
        }

        /// <summary>
        /// We consider another <see cref="SegmentInfo"/> instance equal if it
        /// has the same dir and same name.
        /// </summary>
        public override bool Equals(object obj)
        {
            if (this == obj)
            {
                return true;
            }
            if (obj is SegmentInfo)
            {
                SegmentInfo other = (SegmentInfo)obj;
                return other.Dir == Dir && other.Name.Equals(Name, StringComparison.Ordinal);
            }
            else
            {
                return false;
            }
        }

        public override int GetHashCode()
        {
            return Dir.GetHashCode() + Name.GetHashCode();
        }

        /// <summary>
        /// Used by DefaultSegmentInfosReader to upgrade a 3.0 segment to record its
        /// version is "3.0". this method can be removed when we're not required to
        /// support 3x indexes anymore, e.g. in 5.0.
        /// <para/>
        /// <b>NOTE:</b> this method is used for internal purposes only - you should
        /// not modify the version of a <see cref="SegmentInfo"/>, or it may result in unexpected
        /// exceptions thrown when you attempt to open the index.
        /// <para/>
        /// @lucene.internal
        /// </summary>
        public string Version
        {
            get => version;
            set => this.version = value;
        }

        private ISet<string> setFiles;

        /// <summary>
        /// Sets the files written for this segment. </summary>
        public void SetFiles(ISet<string> files)
        {
            CheckFileNames(files);
            setFiles = files;
        }

        /// <summary>
        /// Add these files to the set of files written for this
        /// segment.
        /// </summary>
        public void AddFiles(ICollection<string> files)
        {
            CheckFileNames(files);
            setFiles.UnionWith(files);
        }

        /// <summary>
        /// Add this file to the set of files written for this
        /// segment.
        /// </summary>
        public void AddFile(string file)
        {
            CheckFileNames(new[] { file });
            setFiles.Add(file);
        }

        private void CheckFileNames(ICollection<string> files)
        {
            Regex r = IndexFileNames.CODEC_FILE_PATTERN;
            foreach (string file in files)
            {
                if (!r.IsMatch(file))
                {
                    throw new ArgumentException("invalid codec filename '" + file + "', must match: " + IndexFileNames.CODEC_FILE_PATTERN.ToString());
                }
            }
        }

        /// <summary>
        /// Get a codec attribute value, or null if it does not exist
        /// </summary>
        [Obsolete("no longer supported")]
        public string GetAttribute(string key)
        {
            if (attributes == null)
            {
                return null;
            }
            else
            {
                string attribute;
                attributes.TryGetValue(key, out attribute);
                return attribute;
            }
        }

        /// <summary>
        /// Puts a codec attribute value.
        /// <para/>
        /// This is a key-value mapping for the field that the codec can use to store
        /// additional metadata, and will be available to the codec when reading the
        /// segment via <see cref="GetAttribute(string)"/>
        /// <para/>
        /// If a value already exists for the field, it will be replaced with the new
        /// value.
        /// </summary>
        [Obsolete("no longer supported")]
        public string PutAttribute(string key, string value)
        {
            if (attributes == null)
            {
                attributes = new Dictionary<string, string>();
            }
            return attributes[key] = value;
        }

        /// <summary>
        /// Returns the internal codec attributes map. May be null if no mappings exist.
        /// </summary>
        [Obsolete("no longer supported")]
        public IDictionary<string, string> Attributes => attributes;
    }
}