using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Objects;
using System.Linq;
using System.Net;
using System.Net.Mail;


namespace Web.Mail
{
    /// <summary>
    /// メール送信機能を提供するクラス
    /// </summary>
    public class SmtpMailClient : MailClient
    {

        /// <summary>
        /// 更新フラグ
        /// </summary>
        public enum SendStatus : int
        {
            Default = 0,
            Success = 1,
            Error = -1,
            Cancell = -2,
        }


        #region 定数

        /// <summary>
        /// メールクライアント数の最大数
        /// </summary>
        private const int MAX_CLIENT_COUNT = 1;

        /// <summary>
        /// 最大のメールクライアント検索回数
        /// </summary>
        private const int MAX_LIMIT_COUNT = 100;

        /// <summary>
        /// メール送信の待機時間(すべてSMTPクライアントがメール送信中の場合)ミリ秒
        /// </summary>
        private const int WAIT_TIME = 2000;

        #endregion

        #region 内部変数


        /// <summary>
        /// 利用しているSMTPクライアント
        /// </summary>
        private List<CustomSmtpClient> Clients = null;
        /// <summary>
        /// 終了フラグ
        /// </summary>
        private bool IsEnd { get; set; }

        /// <summary>
        /// コールバック更新処理
        /// </summary>
        private Action<long, int> UpdateCallback = null;

        #endregion

        #region コンストラクタ

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SmtpMailClient()
            : this(null)
        {

        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SmtpMailClient(Action<long, int> callback)
            : base()
        {
            this.IsEnd = false;
            this.Clients = new List<CustomSmtpClient>();
            this.UpdateCallback = callback;
        }

        #endregion

        #region SMTPサーバ

        /// <summary>
        /// SMTPサーバの取得
        /// </summary>
        /// <param name="mail"></param>
        private CustomSmtpClient CreateSmtpClient()
        {
            var smtpClient = new CustomSmtpClient();


            switch (this.SmtpConnectionType)
            {
                case SmtpConnectionType.SMTPOverSSL:
                    // SMTP over SSLは465版ポート固定
                    smtpClient.EnableSsl = true;
                    smtpClient.Host = this.SmtpServer;
                    smtpClient.Port = 465;

                    break;
                case SmtpConnectionType.STARTTLS:
                    // STARTTLSは"SSL"を付ける
                    smtpClient.EnableSsl = true;
                    smtpClient.Host = this.SmtpServer;
                    smtpClient.Port = this.SmtpPort;
                    break;
                default:
                    smtpClient.EnableSsl = false;
                    smtpClient.Host = this.SmtpServer;
                    smtpClient.Port = this.SmtpPort;
                    break;
            }


            smtpClient.Credentials = new NetworkCredential(this.SmtpUser, this.SmtpPassword);
            smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network;

            //アンチウィルス系のアプリケーションを使用しており、送信されるメールのチェックを行っている場合などでは、
            //SmtpClient.Sendメソッドでメールを送信してもすぐには送信されない場合があるので
            //1ミリ秒にして対応
            smtpClient.ServicePoint.MaxIdleTime = 1;


            //
            // POP before SMTPを使用する場合は、一度POPサーバにログイン
            //
            if (this.PopBeforeSmtp)
            {
                // POPログイン
                ConnectPopBeforeSmtp(this.PopServer, this.PopPort, this.PopUser, this.PopPassword);
            }


            // メールの完了イベント追加
            smtpClient.SendCompleted += new SendCompletedEventHandler(SmtpClient_SendCompleted);

            return smtpClient;
        }

        /// <summary>
        /// SMTPの取得
        /// </summary>
        /// <returns></returns>
        private CustomSmtpClient GetSmtpClient()
        {

            int limitCount = 0;

            while (limitCount < MAX_LIMIT_COUNT)
            {

                // 送信中でないSMTPクライアントの取得
                foreach (var client in this.Clients)
                {
                    // 送信中か否かチェック
                    OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + " SmtpMailClient():Smtp Client Wait[client.IsSending:" + client.IsSending.ToString() + "]\n");
                    if (client.IsSending == false)
                        return client;
                }


                // SMTPクライアントの作成
                if (this.Clients.Count < MAX_CLIENT_COUNT)
                {
                    // SMTPの作成
                    var client = CreateSmtpClient();
                    //追加
                    this.Clients.Add(client);

                    return client;
                }


                limitCount = limitCount + 1;

                // メール送信に時間がかっかているため、一定時間待機
                OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + " SmtpMailClient():Smtp Client Wait[milliSeconds:" + WAIT_TIME.ToString() + "]\n");
                System.Threading.Thread.Sleep(WAIT_TIME);


            }

            OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + " SmtpMailClient():Smtp Client Wait Over[LIMIT_COUNT:" + limitCount.ToString() + "]\n");

            // 一定時間待機しても、メール送信ができない場合、例外を発生させて終了する。
            foreach (var item in this.Clients)
                Release(item);

            throw new Exception("メールの送信処理でタイムアウトが発生しました。");
        }

        #endregion

         /// <summary>
        /// メールを送信する
        /// </summary>
        /// <param name="mail">メールデータ</param>
        /// <returns>送信結果</returns>
        public void SendAsync(Mail mail)
        {
            this.SendAsync(new List<Mail>() { mail });
        }


        /// <summary>
        /// メールを送信する
        /// </summary>
        /// <param name="mail">メールデータ</param>
        /// <returns>送信結果</returns>
        public void SendAsync(List<Mail> mails)
        {
    
            if (this.IsEnd)
            {
                throw new Exception("Not Call 2 times.");
            }

            try
            {

                OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + " SmtpMailClient():Start\n");



                int complete = 0;
                // メッセージの送信
                foreach (var item in mails)
                {

                    CustomSmtpClient smtpClient = GetSmtpClient();
                    OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + " SmtpMailClient():GetSmtpClient[MailDataId:" + item.MailDataId.ToString() + "]\n");


                    if (item.To.Count <= 0)
                    {
                        OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + " SmtpMailClient():item.To.Count <= 0\n");
                        continue;
                    }


                    smtpClient.IsSending = true;

                    // メッセージの作成
                    var mail = new CustomMailMessage();

                    mail.MailDataId = item.MailDataId;
                    
                    // 送信元の設定
                    mail.From = new System.Net.Mail.MailAddress(item.From);

                    //
                    // TO, CC, BCCフィールドの設定
                    //
                    foreach (string strTo in item.To)
                        mail.To.Add(strTo);

                    foreach (string strCc in item.Cc)
                        mail.CC.Add(strCc);

                    foreach (string strBcc in item.Bcc)
                        mail.Bcc.Add(strBcc);

                    System.Text.Encoding enc = System.Text.Encoding.GetEncoding(50220);

                    // 件名
                    mail.Subject = item.Subject;
                    mail.SubjectEncoding = enc;

                    //本文
                    mail.Body = item.Body;
                    mail.BodyEncoding = enc;

                    try
                    {
                        // 同一のSMTPサーバでメールを非同期にメール送信する。
                        smtpClient.SendAsync(mail, mail);

                        // 他スレッドの実行を行う。
                        System.Threading.Thread.Sleep(1);

                        complete++;
                    }
                    catch (Exception ex)
                    {
                        OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "[Error]SmtpMailClient():SendAsync[MailDataId:" + item.MailDataId.ToString() + "] ErroMsg:" + ex.ToString() + "\n");

                        // メールデータの更新
                        this.InvkeUpdateMailData(item.MailDataId, (int)SendStatus.Error);

                        mail.Dispose();

                    }
                }

                OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + " SmtpMailClient():End[SuccessCount:" + complete.ToString() + "]\n");
            }
            catch (Exception ex)
            {
                OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "[Error]SmtpMailClient():Send ErroMsg:" + ex.ToString() + "\n");
                throw new Exception("SmtpMailClient Error", ex);
            }
            finally
            {
                this.IsEnd = true;

                // 送信中以外のリソースを破棄する。
                this.Dispose();
            }
        }

        #region メール送信完了イベント

        /// <summary>
        /// メール送信完了した際にコールバックされます。
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void SmtpClient_SendCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
        {
            var smtpClient = sender as CustomSmtpClient;
            var mailMsg = e.UserState as CustomMailMessage;

            
            int intResult = 0;
            if (e.Cancelled)
            {
                //キャンセル
                OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "[Cancelled]SmtpMailClient():SmtpClient_SendCompleted[MailDataId:" + mailMsg.MailDataId.ToString() + "] \n");

                intResult = (int)SendStatus.Cancell;

            }
            else if(e.Error != null)
            {
                //エラー
                OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "[Error]SmtpMailClient():SmtpClient_SendCompleted[MailDataId:" + mailMsg.MailDataId.ToString() + "] ErroMsg:" + e.Error.ToString() + "\n");

                intResult = (int)SendStatus.Error;

            }
            else
            {
                OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "[Success]SmtpMailClient():SmtpClient_SendCompleted[MailDataId:" + mailMsg.MailDataId.ToString() + "] \n");
                //送信完了
                intResult = (int)SendStatus.Success;
            }

            // メッセージの解放
            mailMsg.Dispose();

            // メールデータの更新
            this.InvkeUpdateMailData(mailMsg.MailDataId, intResult);

            // 終了処理
            this.EndCallback(mailMsg.MailDataId, smtpClient);

            OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "[Success]SmtpMailClient():SmtpClient_SendCompleted End[MailDataId:" + mailMsg.MailDataId.ToString() + "] \n");
        }

        /// <summary>
        /// SMTPクライアントの終了処理を行います。
        /// </summary>
        /// <param name="client"></param>
        private void EndCallback(long mailDataId, CustomSmtpClient client)
        {
            OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "SmtpMailClient():EndCallback Flag:OFF[MailDataId:" + mailDataId.ToString() + "] \n");

            // 送信中のフラグを戻す
            client.IsSending = false;
            OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + " SmtpMailClient():EndCallback[client.IsSending:" + client.IsSending.ToString() + "]\n");
            // メールの送信処理が終了しているかチェック
            if (this.IsEnd)
            {
                // 終了している場合は、SMTPのリソースの破棄を行う。
                Release(client);
            }
        }

        #endregion

        #region 更新処理
         
        /// <summary>
        /// 更新処理を行います。
        /// </summary>
        /// <param name="mailDataId"></param>
        /// <param name="intResult"></param>
        delegate void UpdateDelgate(long mailDataId, int intResult);

        /// <summary>
        /// メール送信の結果を更新します。
        /// </summary>
        /// <param name="mailDataId"></param>
        /// <param name="intResult"></param>
        private void InvkeUpdateMailData(long mailDataId, int intResult)
        {
            OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "SmtpMailClient():InvkeUpdateMailData [MailDataId:" + mailDataId.ToString() + "] \n");

            if (this.UpdateCallback != null)
            {
                // 非同期にて実行
                var thread = new UpdateDelgate(this.UpdateCallback);
                IAsyncResult ar = thread.BeginInvoke(mailDataId, intResult, null, null);
            }


        }

           #endregion

        #region Pop Before Smtp

        /// <summary>
        /// Pop Before Smtp認証のためPOPサーバに接続
        /// </summary>
        /// <param name="serv">POPサーバー</param>
        /// <param name="port">POPポート番号</param>
        /// <param name="user">ユーザID</param>
        /// <param name="pass">パスワード</param>
        public void ConnectPopBeforeSmtp(String serv, int port, String user, String pass)
        {

            try
            {
                String rstr;
                using (var client = new System.Net.Sockets.TcpClient())
                {

                    // POPサーバーに接続
                    client.Connect(serv, port);

                    using (var stream = client.GetStream())
                    {

                        // POPサーバー接続時のレスポンス受信
                        rstr = WriteAndRead(stream, "");
                        if (rstr.IndexOf("+OK") != 0)
                        {
                            throw new Exception("POPサーバー接続エラー");
                        }

                        // ユーザIDの送信
                        rstr = WriteAndRead(stream, "USER " + user + "\r\n");
                        if (rstr.IndexOf("+OK") != 0)
                        {
                            throw new Exception("ユーザIDエラー");
                        }

                        // パスワードの送信
                        rstr = WriteAndRead(stream, "PASS " + pass + "\r\n");
                        if (rstr.IndexOf("+OK") != 0)
                        {
                            throw new Exception("パスワードエラー");
                        }
                        // ステータスの送信
                        rstr = WriteAndRead(stream, "STAT" + "\r\n");
                        if (rstr.IndexOf("+OK") != 0)
                        {
                            throw new Exception("STATエラー");
                        }

                        // 終了の送信
                        rstr = WriteAndRead(stream, "QUIT" + "\r\n");


                        stream.Close();
                        stream.Dispose();
                    }

                    client.Close();
                }
            }
            catch (Exception ex)
            {
                OutputDebugString(System.DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.fff") + "[Error]SmtpMailClient():PopBeforeSmtp ErroMsg:" + ex.ToString() + "\n");
                throw ex;

            }
        }

        /// <summary>
        /// POPサーバ送受信
        /// </summary>
        /// <param name="stm">ストリーム</param>
        /// <param name="req">リクエスト</param>
        /// <returns>レスポンス</returns>
        private String WriteAndRead(System.Net.Sockets.NetworkStream stm, String req)
        {

            // POPサーバへリクエスト送信
            if (req != "")
            {
                Byte[] sdata;
                sdata = System.Text.Encoding.ASCII.GetBytes(req);
                stm.Write(sdata, 0, sdata.Length);
            }
            for (int i = 1; i < 300; i++)
            {
                if (stm.DataAvailable) break;
                System.Threading.Thread.Sleep(10);
            }

            // POPサーバからのレスポンス受信
            String rtn = "";
            Byte[] rdata = new Byte[1024];
            while (stm.DataAvailable)
            {
                int l = stm.Read(rdata, 0, rdata.Length);
                if (l > 0)
                {
                    Array.Resize<Byte>(ref rdata, l);
                    rtn = rtn + System.Text.Encoding.ASCII.GetString(rdata);
                }
            }

            // レスポンス返信
            return rtn;
        }
        #endregion

        #region リソース破棄

        /// <summary>
        /// リソースの破棄を行います。
        /// </summary>
        private void Dispose()
        {
            foreach (var item in this.Clients)
            {
                if (item.IsSending == false)
                    Release(item);

            }

        }

        /// <summary>
        /// SMTPClinetのリリース
        /// </summary>
        /// <param name="smtp"></param>
        private void Release(CustomSmtpClient smtp)
        {
            // QUIT メッセージを SMTP サーバーに送信し、TCP 接続を適切に終了して、すべてのリソースを解放します。
            smtp.SendCompleted -= new SendCompletedEventHandler(SmtpClient_SendCompleted);
            smtp.Dispose();
        }

        #endregion


    }
}