/*
 * Copyright (C) 2014-2023 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * Licensed 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.
 */
package com.helger.css.writer;

import java.io.IOException;
import java.io.Writer;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.WillClose;
import javax.annotation.concurrent.NotThreadSafe;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.io.stream.NonBlockingStringWriter;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.commons.string.StringHelper;
import com.helger.css.ECSSVersion;
import com.helger.css.ICSSWriteable;
import com.helger.css.decl.CSSImportRule;
import com.helger.css.decl.CSSNamespaceRule;
import com.helger.css.decl.CascadingStyleSheet;
import com.helger.css.decl.ICSSTopLevelRule;

/**
 * Convert CSS domain objects back to a text representation.
 *
 * @author Philip Helger
 */
@NotThreadSafe
public class CSSWriter
{
  /** By default optimized output is disabled */
  public static final boolean DEFAULT_OPTIMIZED_OUTPUT = CSSWriterSettings.DEFAULT_OPTIMIZED_OUTPUT;

  private final CSSWriterSettings m_aSettings;
  private boolean m_bWriteHeaderText;
  private String m_sHeaderText = "THIS FILE IS GENERATED - DO NOT EDIT";
  private boolean m_bWriteFooterText;
  private String m_sFooterText;
  private String m_sContentCharset;

  /**
   * Constructor for creating non-optimized output.
   */
  public CSSWriter ()
  {
    this (new CSSWriterSettings ());
  }

  /**
   * Constructor for creating non-optimized output.
   *
   * @param eVersion
   *        The CSS version to emit the code for. May not be <code>null</code> .
   */
  public CSSWriter (@Nonnull final ECSSVersion eVersion)
  {
    this (eVersion, DEFAULT_OPTIMIZED_OUTPUT);
  }

  /**
   * Constructor.
   *
   * @param eVersion
   *        The CSS version to emit the code for. May not be <code>null</code> .
   * @param bOptimizedOutput
   *        If <code>true</code> the number of bytes emitted by this writer is
   *        minimized. Also style rules having no declarations are omitted.
   */
  public CSSWriter (@Nonnull final ECSSVersion eVersion, final boolean bOptimizedOutput)
  {
    this (new CSSWriterSettings (eVersion, bOptimizedOutput));
  }

  /**
   * Constructor
   *
   * @param aSettings
   *        The settings to be used. May not be <code>null</code>.
   */
  public CSSWriter (@Nonnull final CSSWriterSettings aSettings)
  {
    ValueEnforcer.notNull (aSettings, "Settings");
    m_aSettings = aSettings;
    m_bWriteHeaderText = !aSettings.isOptimizedOutput ();
    m_bWriteFooterText = !aSettings.isOptimizedOutput ();
  }

  /**
   * Check if the header text should be emitted. By default it is enabled, if
   * non-optimized output is desired.
   *
   * @return <code>true</code> if the header text should be emitted,
   *         <code>false</code> if not.
   */
  public boolean isWriteHeaderText ()
  {
    return m_bWriteHeaderText;
  }

  /**
   * Determine whether the file header should be written or not. By default it
   * is enabled, if non-optimized output is desired.
   *
   * @param bWriteHeaderText
   *        If <code>true</code> the header text will be written, if
   *        <code>false</code> the text will not be written.
   * @return this
   */
  @Nonnull
  public CSSWriter setWriteHeaderText (final boolean bWriteHeaderText)
  {
    m_bWriteHeaderText = bWriteHeaderText;
    return this;
  }

  /**
   * @return The currently defined header text. May be <code>null</code>.
   */
  @Nullable
  public String getHeaderText ()
  {
    return m_sHeaderText;
  }

  /**
   * Set a custom header text that should be emitted. This text may be multi
   * line separated by the '\n' character. It will emitted if
   * {@link #isWriteHeaderText()} returns <code>true</code>.
   *
   * @param sHeaderText
   *        The header text to be emitted. May be <code>null</code>.
   * @return this
   */
  @Nonnull
  public CSSWriter setHeaderText (@Nullable final String sHeaderText)
  {
    m_sHeaderText = sHeaderText;
    return this;
  }

  /**
   * Check if the footer text should be emitted. By default it is enabled, if
   * non-optimized output is desired.
   *
   * @return <code>true</code> if the footer text should be emitted,
   *         <code>false</code> if not.
   */
  public boolean isWriteFooterText ()
  {
    return m_bWriteFooterText;
  }

  /**
   * Determine whether the file footer should be written or not. By default it
   * is enabled, if non-optimized output is desired.
   *
   * @param bWriteFooterText
   *        If <code>true</code> the footer text will be written, if
   *        <code>false</code> the text will not be written.
   * @return this
   */
  @Nonnull
  public CSSWriter setWriteFooterText (final boolean bWriteFooterText)
  {
    m_bWriteFooterText = bWriteFooterText;
    return this;
  }

  /**
   * @return The currently defined footer text. May be <code>null</code>.
   */
  @Nullable
  public String getFooterText ()
  {
    return m_sFooterText;
  }

  /**
   * Set a custom footer text that should be emitted. This text may be multi
   * line separated by the '\n' character. It will emitted if
   * {@link #isWriteFooterText()} returns <code>true</code>.
   *
   * @param sFooterText
   *        The footer text to be emitted. May be <code>null</code>.
   * @return this
   */
  @Nonnull
  public CSSWriter setFooterText (@Nullable final String sFooterText)
  {
    m_sFooterText = sFooterText;
    return this;
  }

  /**
   * @return The current defined content charset for the CSS. By default it is
   *         <code>null</code>.
   */
  @Nullable
  public String getContentCharset ()
  {
    return m_sContentCharset;
  }

  /**
   * Define the content charset to be used. If not <code>null</code> and not
   * empty, the <code>@charset</code> element is emitted into the CSS. By
   * default no charset is defined.<br>
   * <b>Important:</b> this does not define the encoding of the output - it is
   * just a declarative marker inside the code. Best practice is to use the same
   * encoding for the CSS and the respective writer!
   *
   * @param sContentCharset
   *        The content charset to be used. May be <code>null</code> to indicate
   *        that no special charset name should be emitted into the CSS.
   * @return this
   */
  @Nonnull
  public CSSWriter setContentCharset (@Nullable final String sContentCharset)
  {
    m_sContentCharset = sContentCharset;
    return this;
  }

  /**
   * @return The CSS writer settings that are used to generate the different
   *         element code. This is the same object as passed into/created by the
   *         constructor. Never <code>null</code>.
   */
  @Nonnull
  @ReturnsMutableObject ("Design")
  public CSSWriterSettings getSettings ()
  {
    return m_aSettings;
  }

  /**
   * Write the CSS content to the passed writer. No specific charset is used.
   *
   * @param aCSS
   *        The CSS to write. May not be <code>null</code>.
   * @param aWriter
   *        The write to write the text to. May not be <code>null</code>. Is
   *        automatically closed after the writing!
   * @throws IOException
   *         In case writing fails.
   * @throws IllegalStateException
   *         In case some elements cannot be written in the version supplied in
   *         the constructor.
   * @see #getCSSAsString(CascadingStyleSheet)
   */
  public void writeCSS (@Nonnull final CascadingStyleSheet aCSS, @Nonnull @WillClose final Writer aWriter) throws IOException
  {
    ValueEnforcer.notNull (aCSS, "CSS");
    ValueEnforcer.notNull (aWriter, "Writer");

    try
    {
      final boolean bOptimizedOutput = m_aSettings.isOptimizedOutput ();
      final String sNewLineString = m_aSettings.getNewLineString ();

      // Write file header
      if (m_bWriteHeaderText && StringHelper.hasText (m_sHeaderText))
      {
        aWriter.write ("/*");
        aWriter.write (sNewLineString);
        for (final String sLine : StringHelper.getExploded ("\n", m_sHeaderText))
        {
          aWriter.write (" * " + sLine);
          aWriter.write (sNewLineString);
        }
        aWriter.write (" */");
        aWriter.write (sNewLineString);
      }

      // Charset? Must be the first element before the import
      if (StringHelper.hasText (m_sContentCharset))
      {
        aWriter.write ("@charset \"" + m_sContentCharset + "\";");
        if (!bOptimizedOutput)
          aWriter.write (sNewLineString);
      }

      // Import rules
      int nRulesEmitted = 0;
      final ICommonsList <CSSImportRule> aImportRules = aCSS.getAllImportRules ();
      if (aImportRules.isNotEmpty ())
        for (final CSSImportRule aImportRule : aImportRules)
        {
          aWriter.write (aImportRule.getAsCSSString (m_aSettings));
          ++nRulesEmitted;
        }

      // Namespace rules
      final ICommonsList <CSSNamespaceRule> aNamespaceRules = aCSS.getAllNamespaceRules ();
      if (aNamespaceRules.isNotEmpty ())
        for (final CSSNamespaceRule aNamespaceRule : aNamespaceRules)
        {
          aWriter.write (aNamespaceRule.getAsCSSString (m_aSettings));
          ++nRulesEmitted;
        }

      // Main CSS rules
      for (final ICSSTopLevelRule aRule : aCSS.getAllRules ())
      {
        final String sRuleCSS = aRule.getAsCSSString (m_aSettings);
        if (StringHelper.hasText (sRuleCSS))
        {
          if (!bOptimizedOutput && nRulesEmitted > 0)
            aWriter.write (sNewLineString);

          aWriter.write (sRuleCSS);
          ++nRulesEmitted;
        }
      }

      // Write file footer
      if (m_bWriteFooterText && StringHelper.hasText (m_sFooterText))
      {
        aWriter.write ("/*");
        aWriter.write (sNewLineString);
        for (final String sLine : StringHelper.getExploded ('\n', m_sFooterText))
        {
          aWriter.write (" * " + sLine);
          aWriter.write (sNewLineString);
        }
        aWriter.write (" */");
        aWriter.write (sNewLineString);
      }
    }
    finally
    {
      StreamHelper.close (aWriter);
    }
  }

  /**
   * Create the CSS without a specific charset.
   *
   * @param aCSS
   *        The CSS object to be converted to text. May not be <code>null</code>
   *        .
   * @return The text representation of the CSS.
   * @see #writeCSS(CascadingStyleSheet, Writer)
   */
  @Nonnull
  public String getCSSAsString (@Nonnull final CascadingStyleSheet aCSS)
  {
    final NonBlockingStringWriter aSW = new NonBlockingStringWriter ();
    try
    {
      writeCSS (aCSS, aSW);
    }
    catch (final IOException ex)
    {
      // Should never occur since NonBlockingStringWriter does not throw such an
      // exception
      throw new IllegalStateException ("Totally unexpected", ex);
    }
    return aSW.getAsString ();
  }

  /**
   * Write the CSS content to the passed writer. No specific charset is used.
   *
   * @param aCSS
   *        The CSS to write. May not be <code>null</code>.
   * @param aWriter
   *        The write to write the text to. May not be <code>null</code>. Is
   *        automatically closed after the writing!
   * @throws IOException
   *         In case writing fails.
   * @throws IllegalStateException
   *         In case some elements cannot be written in the version supplied in
   *         the constructor.
   * @see #getCSSAsString(ICSSWriteable)
   */
  public void writeCSS (@Nonnull final ICSSWriteable aCSS, @Nonnull @WillClose final Writer aWriter) throws IOException
  {
    ValueEnforcer.notNull (aCSS, "CSS");
    ValueEnforcer.notNull (aWriter, "Writer");

    try
    {
      aWriter.write (aCSS.getAsCSSString (m_aSettings));
    }
    finally
    {
      StreamHelper.close (aWriter);
    }
  }

  /**
   * Get the string representation of the passed CSS object. It can be any
   * object that implements {@link ICSSWriteable}.
   *
   * @param aCSS
   *        The CSS object to be converted to text. May not be <code>null</code>
   *        .
   * @return The text representation of the CSS.
   * @see #writeCSS(ICSSWriteable, Writer)
   */
  @Nonnull
  public String getCSSAsString (@Nonnull final ICSSWriteable aCSS)
  {
    final NonBlockingStringWriter aSW = new NonBlockingStringWriter ();
    try
    {
      writeCSS (aCSS, aSW);
    }
    catch (final IOException ex)
    {
      // Should never occur since NonBlockingStringWriter does not throw such an
      // exception
      throw new IllegalStateException ("Totally unexpected", ex);
    }
    return aSW.getAsString ();
  }
}
