/*
 * 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.
 */

package org.apache.myfaces.tobago.internal.webapp;

import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
import org.apache.myfaces.tobago.renderkit.html.HtmlTypes;
import org.apache.myfaces.tobago.renderkit.html.MarkupLanguageAttributes;
import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.faces.component.UIComponent;
import java.io.IOException;
import java.io.Writer;
import java.lang.invoke.MethodHandles;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {

  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  protected static final String XML_VERSION_1_0_ENCODING_UTF_8 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";

  protected static final char[] XML_VERSION_1_0_ENCODING_UTF_8_CHARS = XML_VERSION_1_0_ENCODING_UTF_8.toCharArray();

  private int level = 0;

  private int inlineStack = 0;

  private UIComponent component;

  private boolean startStillOpen;

  private final Writer writer;

  private final String contentType;

  private final Charset charset;

  /**
   * @deprecated since 4.3.0
   */
  @Deprecated
  protected TobagoResponseWriterBase(final Writer writer, final String contentType, final String characterEncoding) {
    this(writer, contentType, characterEncoding != null ? Charset.forName(characterEncoding) : StandardCharsets.UTF_8);
  }

  protected TobagoResponseWriterBase(final Writer writer, final String contentType, final Charset charset) {
    this.writer = writer;
    this.contentType = contentType;
    this.charset = charset != null ? charset : StandardCharsets.UTF_8;
  }

  protected final Writer getWriter() {
    return writer;
  }

  protected final UIComponent getComponent() {
    return component;
  }

  protected final void setComponent(final UIComponent component) {
    this.component = component;
  }

  protected final boolean isStartStillOpen() {
    return startStillOpen;
  }

  protected final void setStartStillOpen(final boolean startStillOpen) {
    this.startStillOpen = startStillOpen;
  }

  protected final String findValue(final Object value, final String property) {
    if (value != null) {
      return value instanceof String ? (String) value : value.toString();
    } else if (property != null) {
      if (component != null) {
        final Object object = component.getAttributes().get(property);
        if (object != null) {
          return object instanceof String ? (String) object : object.toString();
        } else {
          return null;
        }
      } else {
        final String trace = getCallingClassStackTraceElementString();
        LOG.warn("Don't know what to do! "
            + "Property defined, but no component to get a value. (value=null, property='" + property + "') "
            + trace.substring(trace.indexOf('(')));
        return null;
      }
    } else {
      final String trace = getCallingClassStackTraceElementString();
      LOG.warn("Don't know what to do! "
          + "No value and no property defined. (value=null, property=null)"
          + trace.substring(trace.indexOf('(')));
      return null;
    }
  }

  @Override
  public void write(final char[] cbuf, final int off, final int len)
      throws IOException {
    writer.write(cbuf, off, len);
  }

  @Override
  public void write(final String string) throws IOException {
    writeInternal(writer, string);
  }

  protected final void writeInternal(final Writer sink, final String string) throws IOException {
    closeOpenTag();
    sink.write(string);
  }

  @Override
  public void write(final int j) throws IOException {
    closeOpenTag();
    writer.write(j);
  }

  @Override
  public void write(final char[] chars) throws IOException {
    closeOpenTag();
    writer.write(chars);
  }

  @Override
  public void write(final String string, final int j, final int k) throws IOException {
    closeOpenTag();
    writer.write(string, j, k);
  }

  @Override
  public void close() throws IOException {
    closeOpenTag();
    writer.close();
  }

  @Override
  public void flush() throws IOException {
    /*
    From the api:
    Flush any ouput buffered by the output method to the underlying Writer or OutputStream.
    This method will not flush the underlying Writer or OutputStream;
    it simply clears any values buffered by this ResponseWriter.
     */
    closeOpenTag();
  }

  protected void closeOpenTag() throws IOException {
    if (startStillOpen) {
      writer.write('>');
      startStillOpen = false;
    }
  }

  @Override
  public void startDocument() throws IOException {
    // nothing to do
  }

  @Override
  public void endDocument() throws IOException {
    // nothing to do
  }

  @Override
  public String getContentType() {
    return contentType;
  }

  @Override
  public String getCharacterEncoding() {
    return charset.name();
  }

  @Override
  public void startElement(final String name, final UIComponent currentComponent) throws IOException {
    final boolean inline = HtmlElements.isInline(name);
    if (inline) {
      inlineStack++;
    }
    this.component = currentComponent;
    startElementInternal(writer, name, HtmlElements.isInline(name));
  }

  @Override
  public void startElement(final HtmlElements name) throws IOException {
    final boolean inline = name.isInline();
    if (inline) {
      inlineStack++;
    }
    startElementInternal(writer, name.getValue(), name.isInline());
    if (!name.isVoid()) {
      level++;
    }
  }

  protected void startElementInternal(final Writer sink, final String name, final boolean inline)
      throws IOException {
    if (startStillOpen) {
      sink.write('>');
    }
    if (inlineStack <= 1) {
      sink.write('\n');
      for (int i = 0; i < level; i++) {
        sink.write(' ');
      }
    }
    sink.write('<');
    sink.write(name);
    startStillOpen = true;
  }

  @Override
  public void endElement(final String name) throws IOException {
    final boolean inline = HtmlElements.isInline(name);
    if (HtmlElements.isVoid(name)) {
      closeEmptyTag();
    } else {
      endElementInternal(writer, name, inline);
    }
    startStillOpen = false;
    if (inline) {
      inlineStack--;
      assert inlineStack >= 0;
    }
  }

  @Override
  public void endElement(final HtmlElements name) throws IOException {
    final boolean inline = name.isInline();
    if (name.isVoid()) {
      closeEmptyTag();
    } else {
      if (!name.isVoid()) {
        level--;
      }
      endElementInternal(writer, name.getValue(), inline);
    }
    startStillOpen = false;
    if (inline) {
      inlineStack--;
      assert inlineStack >= 0;
    }
  }

  @Override
  public void writeComment(final Object obj) throws IOException {
    closeOpenTag();
    final String comment = obj.toString();
    writer.write('\n');
    for (int i = 0; i < level; i++) {
      writer.write(' ');
    }
    write("<!--");
    write(comment);
    write("-->");
  }

  /**
   * @deprecated since 3.0.0
   */
  @Override
  @Deprecated
  public void writeAttribute(final String name, final Object value, final String property)
      throws IOException {

    final String attribute = findValue(value, property);
    writeAttribute(new MarkupLanguageAttributes() {
      @Override
      public String getValue() {
        return name;
      }
    }, attribute, true);
  }

  protected final String getCallingClassStackTraceElementString() {
    final StackTraceElement[] stackTrace = new Exception().getStackTrace();
    int j = 1;
    while (stackTrace[j].getClassName().contains("ResponseWriter")) {
      j++;
    }
    return stackTrace[j].toString();
  }

  @Override
  public void writeURIAttribute(final String name, final Object value, final String property)
      throws IOException {
    if (value != null) {
      final URI uri = URI.create(value.toString());
      writeAttribute(name, uri.toASCIIString(), property);
    }
  }

// interface TobagoResponseWriter //////////////////////////////////////////////////////////////////////////////////

  @Override
  public void writeAttribute(final MarkupLanguageAttributes name, final String value, final boolean escape)
      throws IOException {
    writeAttributeInternal(writer, name, value, escape);
  }

  @Override
  public void writeAttribute(final MarkupLanguageAttributes name, final HtmlTypes types) throws IOException {
    writeAttributeInternal(writer, name, types.getValue(), false);
  }

  @Override
  public void writeURIAttribute(final MarkupLanguageAttributes name, final String value)
      throws IOException {
    if (value != null) {
      final URI uri = URI.create(value);
      writeAttribute(name, uri.toASCIIString(), true);
    }
  }

  protected void endElementInternal(final Writer sink, final String name, final boolean inline) throws IOException {
    if (startStillOpen) {
      sink.write('>');
    }
    if (inline) {
      sink.write("</");
    } else {
      sink.write('\n');
      for (int i = 0; i < level; i++) {
        sink.write(' ');
      }
      sink.write("</");
    }
    sink.write(name);
    sink.write('>');
  }

  protected abstract void closeEmptyTag() throws IOException;

  protected void writeAttributeInternal(
      final Writer sink, final MarkupLanguageAttributes name, final String value, final boolean escape)
      throws IOException {
    if (!startStillOpen) {
      final String trace = getCallingClassStackTraceElementString();
      final String error = "Cannot write attribute when start-tag not open. "
          + "name = '" + name + "' "
          + "value = '" + value + "' "
          + trace.substring(trace.indexOf('('));
      LOG.error(error);
      throw new IllegalStateException(error);
    }

    if (value != null) {
      sink.write(' ');
      sink.write(name.getValue());
      sink.write("='");
      writerAttributeValue(value, escape);
      sink.write('\'');
    }
  }

  protected abstract void writerAttributeValue(String value, boolean escape) throws IOException;


}

