// Copyright (c) 2009-2010 Sun Microsystems, Inc.
//
// MySQL Connector/NET is licensed under the terms of the GPLv2
// <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most 
// MySQL Connectors. There are special exceptions to the terms and 
// conditions of the GPLv2 as it is applied to this software, see the 
// FLOSS License Exception
// <http://www.mysql.com/about/legal/licensing/foss-exception.html>.
//
// This program is free software; you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published 
// by the Free Software Foundation; version 2 of the License.
//
// This program is distributed in the hope that it will be useful, but 
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 
// for more details.
//
// You should have received a copy of the GNU General Public License along 
// with this program; if not, write to the Free Software Foundation, Inc., 
// 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA

using System;
using System.IO;
using System.Diagnostics;
using MySql.Data.Common;

namespace MySql.Data.MySqlClient
{
  /// <summary>
  /// Stream that supports timeout of IO operations.
  /// This class is used is used to support timeouts for SQL command, where a 
  /// typical operation involves several network reads/writes. 
  /// Timeout here is defined as the accumulated duration of all IO operations.
  /// </summary>

  internal class TimedStream : Stream
  {
    Stream baseStream;

    int timeout;
    int lastReadTimeout;
    int lastWriteTimeout;
    LowResolutionStopwatch stopwatch;
    bool isClosed;

    internal bool IsClosed { get { return isClosed; } }

    enum IOKind
    {
      Read,
      Write
    };

    /// <summary>
    /// Construct a TimedStream
    /// </summary>
    /// <param name="baseStream"> Undelying stream</param>
    public TimedStream(Stream baseStream)
    {
      this.baseStream = baseStream;
#if !CF && !RT
      timeout = baseStream.ReadTimeout;
#else
            timeout = System.Threading.Timeout.Infinite;
#endif
      isClosed = false;
      stopwatch = new LowResolutionStopwatch();
    }


    /// <summary>
    /// Figure out whether it is necessary to reset timeout on stream.
    /// We track the current value of timeout and try to avoid
    /// changing it too often, because setting Read/WriteTimeout property
    /// on network stream maybe a slow operation that involves a system call 
    /// (setsockopt). Therefore, we allow a small difference, and do not 
    /// reset timeout if current value is slightly greater than the requested
    /// one (within 0.1 second).
    /// </summary>

    private bool ShouldResetStreamTimeout(int currentValue, int newValue)
    {
      if (newValue == System.Threading.Timeout.Infinite
          && currentValue != newValue)
        return true;
      if (newValue > currentValue)
        return true;
      if (currentValue >= newValue + 100)
        return true;

      return false;

    }
    private void StartTimer(IOKind op)
    {

      int streamTimeout;

      if (timeout == System.Threading.Timeout.Infinite)
        streamTimeout = System.Threading.Timeout.Infinite;
      else
        streamTimeout = timeout - (int)stopwatch.ElapsedMilliseconds;

      if (op == IOKind.Read)
      {
        if (ShouldResetStreamTimeout(lastReadTimeout, streamTimeout))
        {
#if !CF && !RT
          baseStream.ReadTimeout = streamTimeout;
#endif
          lastReadTimeout = streamTimeout;
        }
      }
      else
      {
        if (ShouldResetStreamTimeout(lastWriteTimeout, streamTimeout))
        {
#if !CF && !RT
          baseStream.WriteTimeout = streamTimeout;
#endif
          lastWriteTimeout = streamTimeout;
        }
      }

      if (timeout == System.Threading.Timeout.Infinite)
        return;

      stopwatch.Start();
    }
    private void StopTimer()
    {
      if (timeout == System.Threading.Timeout.Infinite)
        return;

      stopwatch.Stop();

      // Normally, a timeout exception would be thrown  by stream itself, 
      // since we set the read/write timeout  for the stream.  However 
      // there is a gap between  end of IO operation and stopping the 
      // stop watch,  and it makes it possible for timeout to exceed 
      // even after IO completed successfully.
      if (stopwatch.ElapsedMilliseconds > timeout)
      {
        ResetTimeout(System.Threading.Timeout.Infinite);
        throw new TimeoutException("Timeout in IO operation");
      }
    }
    public override bool CanRead
    {
      get { return baseStream.CanRead; }
    }

    public override bool CanSeek
    {
      get { return baseStream.CanSeek; }
    }

    public override bool CanWrite
    {
      get { return baseStream.CanWrite; }
    }

    public override void Flush()
    {
      try
      {
        StartTimer(IOKind.Write);
        baseStream.Flush();
        StopTimer();
      }
      catch (Exception e)
      {
        HandleException(e);
        throw;
      }
    }

    public override long Length
    {
      get { return baseStream.Length; }
    }

    public override long Position
    {
      get
      {
        return baseStream.Position;
      }
      set
      {
        baseStream.Position = value;
      }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
      try
      {
        StartTimer(IOKind.Read);
        int retval = baseStream.Read(buffer, offset, count);
        StopTimer();
        return retval;
      }
      catch (Exception e)
      {
        HandleException(e);
        throw;
      }
    }

    public override int ReadByte()
    {
      try
      {
        StartTimer(IOKind.Read);
        int retval = baseStream.ReadByte();
        StopTimer();
        return retval;
      }
      catch (Exception e)
      {
        HandleException(e);
        throw;
      }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
      return baseStream.Seek(offset, origin);
    }

    public override void SetLength(long value)
    {
      baseStream.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
      try
      {
        StartTimer(IOKind.Write);
        baseStream.Write(buffer, offset, count);
        StopTimer();
      }
      catch (Exception e)
      {
        HandleException(e);
        throw;
      }
    }

    public override bool CanTimeout
    {
      get { return baseStream.CanTimeout; }
    }

    public override int ReadTimeout
    {
      get { return baseStream.ReadTimeout; }
      set { baseStream.ReadTimeout = value; }
    }
    public override int WriteTimeout
    {
      get { return baseStream.WriteTimeout; }
      set { baseStream.WriteTimeout = value; }
    }

#if RT
    public void Close()
#else
    public override void Close()
#endif
    {
      if (isClosed)
        return;
      isClosed = true;
#if !RT
      baseStream.Close();
#endif
#if !CF
      baseStream.Dispose();
#endif
    }

    public void ResetTimeout(int newTimeout)
    {
      if (newTimeout == System.Threading.Timeout.Infinite || newTimeout == 0)
        timeout = System.Threading.Timeout.Infinite;
      else
        timeout = newTimeout;
      stopwatch.Reset();
    }


    /// <summary>
    /// Common handler for IO exceptions.
    /// Resets timeout to infinity if timeout exception is 
    /// detected and stops the times.
    /// </summary>
    /// <param name="e">original exception</param>
    void HandleException(Exception e)
    {
      stopwatch.Stop();
      ResetTimeout(-1);
    }
  }
}
