using System;
using System.IO;

namespace EonaCat.LogStack.Extensions;

// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.

public class OffsetStream : Stream
{
    private const int BufferSize = 4096;

    public OffsetStream(Stream stream, long offset = 0, long length = 0, bool readOnly = false, bool ownStream = false)
    {
        if (stream.CanSeek)
        {
            if (offset > stream.Length)
            {
                throw new EndOfStreamException();
            }

            BaseStreamOffset = offset;

            if (length > stream.Length - offset)
            {
                throw new EndOfStreamException();
            }

            if (length == 0)
            {
                Length1 = stream.Length - offset;
            }
            else
            {
                Length1 = length;
            }
        }
        else
        {
            BaseStreamOffset = 0;
            Length1 = length;
        }

        BaseStream = stream;
        ReadOnly = readOnly;
        OwnStream = ownStream;
    }

    public override bool CanRead => BaseStream.CanRead;

    public override bool CanSeek => BaseStream.CanSeek;

    public override bool CanWrite => BaseStream.CanWrite && !ReadOnly;

    public override long Length => Length1;

    public override long Position
    {
        get => Position1;

        set
        {
            if (value > Length1)
            {
                throw new EndOfStreamException();
            }

            if (!BaseStream.CanSeek)
            {
                throw new NotSupportedException("Cannot seek stream.");
            }

            Position1 = value;
        }
    }

    public long BaseStreamOffset { get; private set; }

    public Stream BaseStream { get; }
    public long Length1 { get; set; }
    public long Position1 { get; set; }

    public bool ReadOnly { get; }

    public bool Disposed { get; set; }

    public bool OwnStream { get; }

    protected override void Dispose(bool disposing)
    {
        if (Disposed)
        {
            return;
        }

        if (disposing)
        {
            if (OwnStream & (BaseStream != null))
            {
                BaseStream.Dispose();
            }
        }

        Disposed = true;

        base.Dispose(disposing);
    }

    public override void Flush()
    {
        if (ReadOnly)
        {
            throw new IOException("OffsetStream is read only.");
        }

        BaseStream.Flush();
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        if (count < 1)
        {
            throw new ArgumentOutOfRangeException("Count cannot be less than 1.");
        }

        if (Position1 >= Length1)
        {
            return 0;
        }

        if (count > Length1 - Position1)
        {
            count = Convert.ToInt32(Length1 - Position1);
        }

        if (BaseStream.CanSeek)
        {
            BaseStream.Position = BaseStreamOffset + Position1;
        }

        var bytesRead = BaseStream.Read(buffer, offset, count);
        Position1 += bytesRead;

        return bytesRead;
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
        if (!BaseStream.CanSeek)
        {
            throw new IOException("Stream is not seekable.");
        }

        long pos;

        switch (origin)
        {
            case SeekOrigin.Begin:
                pos = offset;
                break;

            case SeekOrigin.Current:
                pos = Position1 + offset;
                break;

            case SeekOrigin.End:
                pos = Length1 + offset;
                break;

            default:
                pos = 0;
                break;
        }

        if (pos < 0 || pos >= Length1)
        {
            throw new EndOfStreamException("OffsetStream reached begining/end of stream.");
        }

        Position1 = pos;

        return pos;
    }

    public override void SetLength(long value)
    {
        if (ReadOnly)
        {
            throw new IOException("OffsetStream is read only.");
        }

        BaseStream.SetLength(BaseStreamOffset + value);
        Length1 = value;
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        if (ReadOnly)
        {
            throw new IOException("OffsetStream is read only.");
        }

        if (count < 1)
        {
            return;
        }

        var pos = Position1 + count;

        if (pos > Length1)
        {
            throw new EndOfStreamException("OffsetStream reached end of stream.");
        }

        if (BaseStream.CanSeek)
        {
            BaseStream.Position = BaseStreamOffset + Position1;
        }

        BaseStream.Write(buffer, offset, count);
        Position1 = pos;
    }

    public void Reset(long offset, long length, long position)
    {
        BaseStreamOffset = offset;
        Length1 = length;
        Position1 = position;
    }

    public void WriteTo(Stream stream)
    {
        WriteTo(stream, BufferSize);
    }

    public void WriteTo(Stream stream, int bufferSize)
    {
        if (!BaseStream.CanSeek)
        {
            throw new IOException("Stream is not seekable.");
        }

        if (Length1 < bufferSize)
        {
            bufferSize = Convert.ToInt32(Length1);
        }

        var previousPosition = Position1;
        Position1 = 0;

        try
        {
            CopyTo(stream, bufferSize);
        }
        finally
        {
            Position1 = previousPosition;
        }
    }
}