/*************************************************************************************

   Extended WPF Toolkit

   Copyright (C) 2007-2013 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://wpftoolkit.codeplex.com/license 

   For more features, controls, and fast professional support,
   pick up the Plus Edition at http://xceed.com/wpf_toolkit

   Stay informed: follow @datagrid on Twitter or Like http://facebook.com/datagrids

  ***********************************************************************************/

using System.Security;
using System.Windows;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Input;
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace Xceed.Wpf.Toolkit
{
  public class WatermarkPasswordBox : WatermarkTextBox
  {
    #region Members

    private int _newCaretIndex = -1;

    #endregion

    #region Properties

    #region Password

    public string Password
    {
      [SecuritySafeCritical]
      get
      {
        string passwordString;
        var valuePtr = Marshal.SecureStringToBSTR( this.SecurePassword );
        try
        {
          passwordString = Marshal.PtrToStringUni( valuePtr );
        }
        finally
        {
          Marshal.ZeroFreeBSTR( valuePtr );
        }
        return passwordString;
      }
      set
      {
        if( value == null )
        {
          value = string.Empty;
        }
        this.SecurePassword = new SecureString();
        for( int i = 0; i < value.Length; ++i )
        {
          this.SecurePassword.AppendChar( value[ i ] );
        }

        // Internal changes to Password property will have a _newCaretIndex > 0.
        this.SyncTextPassword( _newCaretIndex );

        this.RaiseEvent( new RoutedEventArgs( PasswordChangedEvent, this ) );
      }
    }

    #endregion

    #region PasswordChar

    public static readonly DependencyProperty PasswordCharProperty = DependencyProperty.Register( "PasswordChar", typeof( char ), typeof( WatermarkPasswordBox )
      , new UIPropertyMetadata( '\u25CF', OnPasswordCharChanged ) ); //default is black bullet

    public char PasswordChar
    {
      get
      {
        return (char)GetValue( PasswordCharProperty );
      }

      set
      {
        SetValue( PasswordCharProperty, value );
      }
    }

    private static void OnPasswordCharChanged( DependencyObject o, DependencyPropertyChangedEventArgs e )
    {
      var watermarkPasswordBox = o as WatermarkPasswordBox;
      if( watermarkPasswordBox != null )
      {
        watermarkPasswordBox.OnPasswordCharChanged( (char)e.OldValue, (char)e.NewValue );
      }
    }

    protected virtual void OnPasswordCharChanged( char oldValue, char newValue )
    {
      this.SyncTextPassword( this.CaretIndex );
    }

    #endregion

    #region SecurePassword

    public SecureString SecurePassword
    {
      get;
      private set;
    }

    #endregion SecurePassword

    #endregion //Properties

    #region Constructors

    public WatermarkPasswordBox()
    {
      this.Password = string.Empty;
      this.IsUndoEnabled = false;
      this.UndoLimit = 0;

      CommandManager.AddPreviewCanExecuteHandler( this, OnPreviewCanExecuteCommand );
      DataObject.AddPastingHandler( this, OnPaste );
    }

    #endregion //Constructors

    #region Base Class Overrides

    [SecuritySafeCritical]
    protected override void OnPreviewTextInput( TextCompositionEventArgs e )
    {
      this.PasswordInsert( e.Text, this.CaretIndex );

      e.Handled = true; //Handle to prevent TextChanged when OnPreviewTextInput exist

      base.OnPreviewTextInput( e );
    }

    [SecuritySafeCritical]
    protected override void OnPreviewKeyDown( KeyEventArgs e )
    {
      // Keys not detected by OnPreviewTextInput
      switch( e.Key )
      {
        case Key.Space:
          this.PasswordInsert( " ", this.CaretIndex );
          e.Handled = true;  //Handle to prevent TextChanged when OnPreviewKeyDown exist
          break;
        case Key.Back:
          // With a selection, delete from CaretIndex. Without a selection delete the character before the CaretIndex.
          this.PasswordRemove( (this.SelectedText.Length > 0) ? this.CaretIndex : this.CaretIndex - 1 );
          e.Handled = true;  //Handle to prevent TextChanged when OnPreviewKeyDown exists
          break;
        case Key.Delete:
          this.PasswordRemove( this.CaretIndex );
          e.Handled = true;  //Handle to prevent TextChanged when OnPreviewKeyDown exist
          break;
        case Key.V:
          if( (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control )
          {
            if( Clipboard.ContainsText() )
            {
              this.PasswordInsert( Clipboard.GetText(), this.CaretIndex );
              e.Handled = true; //Handle to prevent TextChanged when OnPreviewKeyDown exist
            }
          }
          break;
      }

      base.OnPreviewKeyDown( e );
    }

    protected override void OnTextChanged( TextChangedEventArgs e )
    {
      base.OnTextChanged( e );

      if( this.Text.Length != this.Password.Length )
      {
        // When Clear() or Cut() methods are called, we need to update the Password property to empty.
        if( this.Text == "" )
        {
          this.SetPassword( "", 0 );
        }
        // When AppendText() method is called, we need to reset the Text property to prevent adding text.
        else
        {
          this.SyncTextPassword( this.Password.Length );
        }
      }
    }

    #endregion //Base Class Overrides

    #region Event

    public static readonly RoutedEvent PasswordChangedEvent = EventManager.RegisterRoutedEvent( "PasswordChanged", RoutingStrategy.Bubble, typeof( RoutedEventHandler )
      , typeof( WatermarkPasswordBox ) );
    public event RoutedEventHandler PasswordChanged
    {
      add
      {
        AddHandler( PasswordChangedEvent, value );
      }
      remove
      {
        RemoveHandler( PasswordChangedEvent, value );
      }
    }

    #endregion

    #region Event Handlers

    [ SecuritySafeCritical]
    private void OnPaste( object sender, DataObjectPastingEventArgs e )
    {
      //Pasting something that is not text
      if( !e.SourceDataObject.GetDataPresent( DataFormats.UnicodeText, true ) )
        return;

      var text = e.SourceDataObject.GetData( DataFormats.UnicodeText ) as string;
      if( text != null )
      {
        this.PasswordInsert( text, this.CaretIndex );
      }
      e.CancelCommand(); //Cancel to prevent TextChanged
    }

    private void OnPreviewCanExecuteCommand( object sender, CanExecuteRoutedEventArgs e )
    {
      //Will not execute these actions
      if( e.Command == ApplicationCommands.Copy ||
          e.Command == ApplicationCommands.Cut ||
          e.Command == ApplicationCommands.Undo )
      {
        e.CanExecute = false;
        e.Handled = true;  //Handle to prevent actions
      }
    }

    #endregion

    #region Private Methods

    [SecurityCritical]
    private void PasswordInsert( string text, int index )
    {
      if( text == null )
        return;
      if( (index < 0) || (index > this.Password.Length) )
        return;

      //If there is a selection, remove it first
      if( this.SelectedText.Length > 0 )
      {
        this.PasswordRemove( index );
      }

      var newPassword = this.Password;
      for( int i = 0; i < text.Length; ++i )
      {
        // MaxLength == 0 is no limit
        if( (this.MaxLength == 0) || (newPassword.Length < this.MaxLength) )
        {
          newPassword = newPassword.Insert( index++, text[ i ].ToString() );
        }
      }
      this.SetPassword( newPassword, index );
    }

    [SecurityCritical]
    private void PasswordRemove( int index )
    {
      if( (index < 0) || (index >= this.Password.Length) )
        return;

      if( this.SelectedText.Length > 0 )
      {
        var newPassword = this.Password;
        for( int i = 0; i < this.SelectedText.Length; ++i )
        {
          newPassword = newPassword.Remove( index, 1 );          
        }
        this.SetPassword( newPassword, index );
      }
      else
      {
        var newPassword = this.Password.Remove( index, 1 );
        this.SetPassword( newPassword, index );
      }
    }

    private void SetPassword( string password, int caretIndex )
    {
      _newCaretIndex = caretIndex;
      this.Password = password;
      _newCaretIndex = -1;
    }

    private void SyncTextPassword( int nextCarretIndex )
    {
      var sb = new StringBuilder();
      this.Text = sb.Append( Enumerable.Repeat(this.PasswordChar, this.Password.Length).ToArray() ).ToString();
      //set CaretIndex after Text is changed
      this.CaretIndex = Math.Max( nextCarretIndex, 0 );
    }

    #endregion
  }
}
