//
// Copyright 2022 Google LLC
//
// 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 Google.Solutions.Common.Util;
using Google.Solutions.Ssh.Native;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace Google.Solutions.Ssh
{
    /// <summary>
    /// Channel for interacting with remote file.
    /// </summary>
    public class SftpChannel : SshChannelBase, ISftpChannel
    {
        /// <summary>
        /// Recommended buffer size to use for reading from, or
        /// writing to a stream.
        ///
        /// SFTP effectively limits the size of a packet to 32 KB, see
        /// <https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-4>
        ///
        /// libssh2 uses a slightly smaller limit of 30000 bytes 
        /// (MAX_SFTP_OUTGOING_SIZE, MAX_SFTP_READ_SIZE).
        /// Using a buffer larger than 30000 bytes therefore doen't
        /// provide much value.
        ///
        /// Note that IAP/SSH Relay uses 16KB as maximum message size,
        /// so a 32000 byte packet will be split into 2 messages. 
        /// That's still more efficient than using a SFTP packet
        /// size below 16 KB as it at least limits the number of 
        /// SSH_FXP_STATUS packets that need to be exchanged.
        ///
        /// </summary>
        public const int BufferSize = 30000;

        /// <summary>
        /// Channel handle, must only be accessed on worker thread.
        /// </summary>
        private readonly Libssh2SftpChannel nativeChannel;

        /// <summary>
        /// Connection used by this channel.
        /// </summary>
        public override SshConnection Connection { get; }

        internal SftpChannel(
            SshConnection connection,
            Libssh2SftpChannel nativeChannel)
        {
            this.Connection = connection;
            this.nativeChannel = nativeChannel;
        }

        //---------------------------------------------------------------------
        // Overrides.
        //---------------------------------------------------------------------

        protected override void Close()
        {
            Debug.Assert(this.Connection.IsRunningOnWorkerThread);

            this.nativeChannel.Dispose();
        }

        internal override void OnReceive()
        {
            //
            // We're never expecting any unsolicited data from the
            // server, so ignore the callback.
            //
        }

        internal override void OnReceiveError(Exception exception)
        {
        }

        //---------------------------------------------------------------------
        // Publics.
        //---------------------------------------------------------------------

        /// <summary>
        /// List contents of a directory.
        /// </summary>
        public Task<IReadOnlyCollection<SftpFileInfo>> ListFilesAsync(
            string remotePath)
        {
            Precondition.ExpectNotEmpty(remotePath, nameof(remotePath));

            return this.Connection.RunAsync(
                c =>
                {
                    Debug.Assert(this.Connection.IsRunningOnWorkerThread);

                    using (c.Session.AsBlocking())
                    {
                        return this.nativeChannel.ListFiles(remotePath);
                    }
                },
                false);
        }

        /// <summary>
        /// Create or open a file.
        /// </summary>
        public Task<Stream> CreateFileAsync(
            string remotePath,
            FileMode mode,
            FileAccess access,
            FilePermissions permissions)
        {
            Precondition.ExpectNotEmpty(remotePath, nameof(remotePath));

            var flags = mode switch
            {
                FileMode.Create => LIBSSH2_FXF_FLAGS.CREAT,
                FileMode.CreateNew => LIBSSH2_FXF_FLAGS.CREAT | LIBSSH2_FXF_FLAGS.EXCL,
                FileMode.OpenOrCreate => LIBSSH2_FXF_FLAGS.CREAT,
                FileMode.Open => (LIBSSH2_FXF_FLAGS)0,
                FileMode.Truncate => LIBSSH2_FXF_FLAGS.TRUNC,
                FileMode.Append => LIBSSH2_FXF_FLAGS.APPEND,

                _ => throw new ArgumentException("The file mode is invalid"),
            };

            if (access.HasFlag(FileAccess.Read))
            {
                flags |= LIBSSH2_FXF_FLAGS.READ;
            }

            if (access.HasFlag(FileAccess.Write))
            {
                flags |= LIBSSH2_FXF_FLAGS.WRITE;
            }

            return this.Connection.RunAsync<Stream>(
                c =>
                {
                    Debug.Assert(this.Connection.IsRunningOnWorkerThread);

                    using (c.Session.AsBlocking())
                    {
                        return new SftpFileStream(
                            this.Connection,
                            this.nativeChannel.CreateFile(
                                remotePath,
                                flags,
                                permissions),
                            flags);
                    }
                },
                false);
        }
    }
}
