using System;
using System.IO;
using System.Text;
using System.Collections.Generic;

using CommonHelpers;

namespace SqlCatPP {

//**************************************************************************************************
partial class Scripter {
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

public const int
   GenOrderPlaces    = 4,
   GenOrderMaxIndex  = 999,
   GenOrderGroupSize = 1000; // GNNN.<FileName>.sql

//--------------------------------------------------------------------------------------------------

bool HasParent { get; }

bool UnfoldEnvironmentVars() =>
   Program.ProcessDirectives || Program.UnfoldEnvironmentVars;
static bool ProcessDirectives() =>
   Program.ProcessDirectives;

bool PerformGeneration =>
   !HasParent && GeneratedParams.GenerationOn && GeneratedParams.FolderPath != null;
bool PerformGenerationWithOrder =>
   PerformGeneration && Program.IsMultipleTranslation && GeneratedParams.OrderedWithCleanup;

bool CleanAllSQL() =>
   PerformGeneration && Program.IsMultipleTranslation && GeneratedParams.CleanAllSQL;
bool WithPreliminaryCleanup() =>
   PerformGeneration && Program.IsMultipleTranslation && (
   GeneratedParams.CleanAllSQL ||
   PerformGenerationWithOrder && !GeneratedParams.RestoreDateIfSameContent );
bool WithPosteriorCleanup() =>
   PerformGeneration && Program.IsMultipleTranslation &&
   !GeneratedParams.CleanAllSQL && !WithPreliminaryCleanup() &&
   PerformGenerationWithOrder && GeneratedParams.RestoreDateIfSameContent;
bool EmbedFileNamesToOrder() =>
   PerformGeneration && GeneratedParams.EmbedFileNamesToOrder && !WithPreliminaryCleanup();
bool RestoreDateIfSameContent() =>
   PerformGeneration && !CleanAllSQL() && GeneratedParams.RestoreDateIfSameContent;

static int GenOrderIndex =>
   Program.TotallyScriptFiles;
/// <exception cref="InvalidOperationException" />
static string GenFolderPath =>
   GeneratedParams.FolderPath ?? throw new InvalidOperationException(
      "Property is null: \"GeneratedParams.FolderPath\"." );
static GeneratedParams GeneratedParams =>
   Program.GeneratedParams;

//==================================================================================================

/// <excepton cref="OperationFailedException" />
/// <excepton cref="SystemException" />
public Scripter()
{
   _ScriptWriter_ = new StringWriter();
   _SetVariables_ = new Dictionary<string,string>( StringComparer.InvariantCultureIgnoreCase );
   if ( !PerformGeneration )
      return;
   if ( !Directory.Exists( GenFolderPath ) )
      Directory.CreateDirectory( GenFolderPath );
   string[] genFilePaths = null;
   OpFailedExceptionCreator.Do(() =>
      genFilePaths = Directory.GetFiles( GenFolderPath, "*.sql" ));
   var genFileNameList = new List<string>();
   _GenFileTable_ = new Dictionary<string,GenFileData>(
      StringComparer.InvariantCultureIgnoreCase );
   foreach (string filePath in genFilePaths)
   {
      string fileName = Path.GetFileName( filePath );
      genFileNameList.Add( fileName );
      _GenFileTable_[ fileName ] = new GenFileData( filePath );
   }
   genFileNameList.Sort( CaseInsensitiveComparerStr.Instance );
   _GenFileNames_ = genFileNameList.ToArray();
   if ( WithPreliminaryCleanup() )
      CleanGeneratedPreliminary();
}

/// <exception cref="ArgumentNullException" />
public Scripter(Scripter parent)
{
   HArgChecking.VerifyNotNull( parent );
   HasParent = true;
   _ScriptWriter_ = parent._ScriptWriter_;
   _CurrentScriptWriter_ = parent._CurrentScriptWriter_;
   _SetVariables_ = parent._SetVariables_;
}

//--------------------------------------------------------------------------------------------------

/// <exception cref="ArgumentNullException" />
/// <exception cref="OperationFailedException" />
public void ProcessSourceFile(string inputFilePath)
{
   FileNameHlp.VerifyFileExists( inputFilePath );
   void ResetInputPN() =>
      _InputFullPath_ = _InputFileName_ = null;
   if ( !HasParent )
   {
      if ( Program.ProcessDirectives )
         _SetVariables_.Clear();
      if ( PerformGenerationWithOrder && GenOrderIndex > GenOrderMaxIndex )
         throw OpFailedExceptionCreator.Create(
            "Too many files for ordered generation.\n"+
            "Gen.order group size is: {0}.", GenOrderGroupSize );
   }
   bool hasException = false;
   try
   {
      _InputFullPath_ = Path.GetFullPath( inputFilePath );
      _InputFileName_ = Path.GetFileName( inputFilePath );
      DoProcessSourceFile( inputFilePath );
   }
   catch (Exception e)
   {
      hasException = true;
      if ( _InputFullPath_ == null )
         throw OpFailedExceptionCreator.Create( e );
      throw OpFailedExceptionCreator.Create(
         "{0}\n"+
         "File: \"{1}\".",
         e.Message, _InputFullPath_ );
   }
   finally
   {
      if ( hasException ) ResetInputPN();
   }
   try
   {
      if ( HasParent )
         return;
      if ( PerformGeneration )
      {
         string fileName = Path.GetFileName( inputFilePath );
         if ( !HString.Equals( Path.GetExtension( fileName ), ".sql", true ) )
            fileName += ".sql";
         if ( PerformGenerationWithOrder )
            fileName = string.Format( "{0}{1}.{2}",
               GeneratedParams.OrderHeadingChar,
               HInt32.ToNormalString( GenOrderIndex ).PadLeft( GenOrderPlaces-1, '0' ),
               fileName.StartsWith(".") || fileName.StartsWith("!") ?
                  fileName.Remove( 0, 1 ) : fileName );
         CompleteCurrentOutput(Path.Combine( GeneratedParams.FolderPath, fileName ));
      }
      Program.IncrementTotallyScriptFiles();
   }
   finally
   {
      ResetInputPN();
   }
}
/// <exception cref="SystemException" />
/// <exception cref="SqlPPException" />
/// <exception cref="OperationFailedException" />
void DoProcessSourceFile(string inputFilePath)
{
   ResetParseState();
   StreamReader reader = null;
   Exception error = null;
   try
   {
      reader = new StreamReader( inputFilePath, Program.InputEncoding, true );
      if ( !HasParent )
         _CurrentScriptWriter_ = PerformGeneration ? new StringWriter() : null;
      bool
         processDirectives = Program.ProcessDirectives,
         insertSetVarCommentsOnly = Program.InsertSetVarComments && !processDirectives,
         usePP = processDirectives || insertSetVarCommentsOnly;
      string str;
      while ( (str = reader.ReadLine()) != null )
      {
         _LineNo_++;
         if ( Program.CheckInputForANSI )
            for ( int i=0; i<str.Length; i++ )
            {
               char ch = str[i];
               if ( !HChar.IsInSystemACP( ch ) )
                  throw CreatePPError( i,
                     "Character 0x{0:X4} does not comply to ANSI code-page (current system CP).",
                     (int)ch );
            }
         if ( usePP )
         {
            SetVarCommentInclusion inclusion;
            bool
               processDirectivesHere = processDirectives && !IsInCommentOrStr,
               isSetVarDirectiveLine = IsSetVarDirectiveLine_Remember( str, out inclusion ),
               isRDirectiveLine = IsRDirectiveLine_IncludeFile( str ),
               isSupportedDirectiveLine = isSetVarDirectiveLine || isRDirectiveLine;
            if ( isSupportedDirectiveLine && ( processDirectivesHere || (
                 insertSetVarCommentsOnly && (
                 inclusion == SetVarCommentInclusion.Anywhere ||
                 inclusion == SetVarCommentInclusion.ScriptAnalisys && !IsInCommentOrStr ))))
            {
               if ( isSetVarDirectiveLine )
               {
                  if ( insertSetVarCommentsOnly )
                     WriteOutputLine( str );
                  WriteOutput( "--" );
                  WriteOutputLine( str );
               }
               continue;
            }
            if ( !isSupportedDirectiveLine && UnfoldEnvironmentVars() )
               if ( processDirectives )
                  str = ParseSqlLine( str );
               else
                  ParseSqlLine( str );
         }
         WriteOutputLine( str );
      }
   }
   catch (Exception e)
   {
      error = e; throw;
   }
   finally
   {
      if ( reader != null )
         try
         {
            reader.Close(); reader.Dispose();
         }
         catch (Exception e)
         {
            if ( error == null )
               throw OpFailedExceptionCreator.Create( error = e );
         }
   }
}

void WriteOutput(string text)
{
   _ScriptWriter_.Write( text );
   _CurrentScriptWriter_?.Write( text );
}
void WriteOutputLine(string lineText)
{
   _ScriptWriter_.WriteLine( lineText );
   _CurrentScriptWriter_?.WriteLine( lineText );
}

//--------------------------------------------------------------------------------------------------

/// <exception cref="ArgumentNullException" />
/// <exception cref="SqlPPException" />
bool IsSetVarDirectiveLine_Remember(string str,out SetVarCommentInclusion inclusion)
{
   HArgChecking.VerifyNotNull( str );
   inclusion = SetVarCommentInclusion.None;
   int len = str.Length, i = 0;
   while ( i<len && str[i]==' ' )
      i++;
   if ( i==len || str[i++]!=':' )
      return false;
   int spacePos = str.IndexOf( ' ', i );
   string directive = spacePos>=0 ? str.Substring( i, spacePos-i ) : str.Substring( i );
   if ( directive == "SetVar" ) {
      inclusion = SetVarCommentInclusion.ScriptAnalisys;
      goto SET_VAR; }
   if ( directive == "SetVAR" ) {
      inclusion = SetVarCommentInclusion.Anywhere;
      goto SET_VAR; }
   if ( !HString.Equals( directive, "setvar", true ) )
      return false;
SET_VAR:
   if ( !UnfoldEnvironmentVars() || IsInCommentOrStr )
      return true;
   str = str.Substring( spacePos+1 ).Trim();
   len = str.Length;
   spacePos = str.IndexOf( ' ' );
   string variableName = spacePos>=0 ? str.Substring( 0, spacePos ) : str;
   if ( !IsNormalAsciiIdent( variableName ) )
      goto INCORRECT;
   if ( spacePos < 0 )
      goto SET_NULL;
   int p = spacePos+1;
   for ( ; p<len && str[p]==' '; p++ )
   {}
   if ( p==len || str.IndexOf("--",p)==p )
      goto SET_NULL;
   str = str.Substring( spacePos+1 ).TrimStart();
   len = str.Length;
   if (!( len>=2 && str[0]=='"' ))
      goto INCORRECT;
   int pNextDQ = str.IndexOf( '"', 1 );
   if ( pNextDQ>=0 && pNextDQ+1 < len )
   {
      for ( i=pNextDQ+1; i<len && str[i]==' '; i++ )
      {}
      if ( i+1<len && str[i]=='-' && str[i+1]=='-' )
         str = str.Remove( pNextDQ+1 );
   }
   len = str.Length;
   if (!( len>=2 && str[0]=='"' && HString.LastChar( str )=='"' ))
      goto INCORRECT;
   len -= 2;
   str = len>0 ? str.Substring(1,len) : "";
   var sb = new StringBuilder();
   for ( i=0; i<len; )
   {
      char ch = str[i];
      if ( ch=='"' && !( ++i<len && str[i]=='"' ) )
         goto INCORRECT;
      sb.Append( ch );
      i++;
   }
   _SetVariables_[ variableName ] = sb.ToString();
   return true;
SET_NULL:
   _SetVariables_[ variableName ] = null;
   return true;
INCORRECT:
   return Program.ProcessDirectives ?
      throw CreatePPError( "Incorrent \":SetVar\"-directive." ) :
      true;
}

//--------------------------------------------------------------------------------------------------

/// <exception cref="ArgumentNullException" />
/// <exception cref="SqlPPException" />
/// <exception cref="SystemException" />
bool IsRDirectiveLine_IncludeFile(string str)
{
   HArgChecking.VerifyNotNull( str );
   int len = str.Length, i = 0;
   while ( i<len && str[i]==' ' )
      i++;
   if ( i==len || str[i++]!=':' )
      return false;
   int spacePos = str.IndexOf( ' ', i );
   string directive = spacePos>=0 ? str.Substring( i, spacePos-i ) : str.Substring( i );
   if ( !HString.Equals( directive, "r", true ) )
      return false;
   if ( !Program.ProcessDirectives || IsInCommentOrStr )
      return true;
   str = str.Substring( spacePos+1 ).Trim();
   len = str.Length;
   if ( len == 0 )
      goto INCORRECT;
   bool unfold = false;
   if ( len>=2 && str[0]=='"' && HString.LastChar( str )=='"' )
   {
      str = str.Substring( 1, len-2 );
      if ( str.IndexOf( '"' ) >= 0 )
         goto INCORRECT;
   }
   else
      unfold = true;
   if ( !Program.ProcessDirectives )
      return true;
   var filePath = unfold ? UnfoldRDirectiveBody( str ) : str;
   if ( Program.ProcessDirectives && HasParent )
      throw CreatePPError(
         "Script inclusion into child script is not supported "+
         "in autonomous directive processing." );
   IncludeScript( filePath );
   return true;
INCORRECT:
   return Program.ProcessDirectives ?
      throw IncorrectRDirective_Error() : true;
}

/// <exception cref="ArgumentNullException" />
/// <exception cref="InvalidOperationException" />
public void IncludeScript(string inputFilePath)
{
   HArgChecking.VerifyNotNull( inputFilePath );
   VerifyHasScriptWriter();
   try
   {
      inputFilePath = Path.GetFullPath( inputFilePath );
   }
   catch (Exception e)
   {
      throw CreatePPError(
         "Can not obtain full path for child inclusion (file-name: \"{0}\").\n"+
         "{1}", inputFilePath, e.Message );
   }
   if ( HString.EqualsInvariant( inputFilePath, _InputFullPath_, true ) )
      throw CreatePPError(
         "Attempt to recursively include source script.");
   new Scripter( this ).ProcessSourceFile( inputFilePath );
}

//==================================================================================================

StringWriter _ScriptWriter_, _CurrentScriptWriter_;
Dictionary<string,string> _SetVariables_;
string[] _GenFileNames_;
Dictionary<string,GenFileData> _GenFileTable_;

string _InputFullPath_, _InputFileName_;
int _LineNo_;

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
} // Scripter
//**************************************************************************************************

} // SqlCatPP
