/*
 * Copyright 2015 Google Inc.
 *
 * 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.google.template.soy.jbcsrc.runtime;

import static com.google.common.base.Preconditions.checkState;

import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueProvider;
import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
import com.google.template.soy.data.restricted.StringData;
import com.google.template.soy.jbcsrc.api.AdvisingAppendable;
import com.google.template.soy.jbcsrc.api.AdvisingStringBuilder;
import com.google.template.soy.jbcsrc.api.RenderResult;

import java.io.IOException;

import javax.annotation.Nullable;

/**
 * A special implementation of {@link SoyValueProvider} to use as a shared base class for the
 * {@code jbcsrc} implementations of the generated {@code LetContentNode} and 
 * {@code CallParamContentNode} implementations.
 */
public abstract class DetachableContentProvider implements SoyValueProvider {
  @Nullable private final ContentKind contentKind;
  
  // Will be either a SanitizedContent or a StringData, but there is no common super type besides
  // this.
  private SoyValue resolvedValue;

  // Will be either an AdvisingStringBuilder or a TeeAdvisingAppendable depending on whether we are
  // being resolved via 'status()' or via 'renderAndResolve'
  private AdvisingAppendable builder;
  
  protected DetachableContentProvider(@Nullable ContentKind contentKind) {
    this.contentKind = contentKind;
  }

  @Override public final SoyValue resolve() {
    SoyValue local = resolvedValue;
    checkState(local != null, "called resolve() before status() returned ready.");
    checkState(local != TombstoneValue.INSTANCE,
        "called resolve() after calling renderAndResolve with isLast == true");
    return local;
  }

  @Override public final RenderResult status() {
    if (resolvedValue != null) {
      return RenderResult.done();
    }
    AdvisingStringBuilder currentBuilder = (AdvisingStringBuilder) builder;
    if (currentBuilder == null) {
      builder = currentBuilder = new AdvisingStringBuilder();
    }
    return doRenderIntoBufferingAppendable(currentBuilder);
  }

  @Override public RenderResult renderAndResolve(AdvisingAppendable appendable, boolean isLast)
      throws IOException {
    SoyValue value = resolvedValue;
    if (value != null) {
      value.render(appendable);
      return RenderResult.done();
    }
    if (isLast) {
      RenderResult result = doRender(appendable);
      if (result.isDone()) {
        resolvedValue = TombstoneValue.INSTANCE;
      }
      return result;
    }
    TeeAdvisingAppendable currentBuilder = (TeeAdvisingAppendable) builder;
    if (currentBuilder == null) {
      builder = currentBuilder = new TeeAdvisingAppendable(appendable);
    }
    return doRenderIntoBufferingAppendable(currentBuilder);
  }

  private RenderResult doRenderIntoBufferingAppendable(AdvisingAppendable target) {
    RenderResult result = doRender(target);
    if (result.isDone()) {
      if (contentKind != null) {
        resolvedValue = UnsafeSanitizedContentOrdainer.ordainAsSafe(target.toString(), contentKind);
      } else {
        resolvedValue = StringData.forValue(target.toString()); 
      }
    }
    return result;
  }

  /** Overridden by generated subclasses to implement lazy detachable resolution. */
  protected abstract RenderResult doRender(AdvisingAppendable appendable);

  /**
   * An {@link AdvisingAppendable} that forwards to a delegate appendable but also saves all the
   * same forwarded content into a buffer.
   *
   * <p>See: <a href="http://en.wikipedia.org/wiki/Tee_%28command%29">Tee command</p> for the unix
   * command on which this is based.
   */
  private static final class TeeAdvisingAppendable implements AdvisingAppendable {
    final StringBuilder buffer = new StringBuilder();
    final AdvisingAppendable delegate;

    TeeAdvisingAppendable(AdvisingAppendable delegate) {
      this.delegate = delegate;
    }

    @Override public AdvisingAppendable append(CharSequence csq) throws IOException {
      delegate.append(csq);
      buffer.append(csq);
      return this;
    }

    @Override public AdvisingAppendable append(CharSequence csq, int start, int end) 
        throws IOException {
      delegate.append(csq, start, end);
      buffer.append(csq, start, end);
      return this;
    }

    @Override public AdvisingAppendable append(char c) throws IOException {
      delegate.append(c);
      buffer.append(c);
      return this;
    }

    @Override public boolean softLimitReached() {
      return delegate.softLimitReached();
    }
    
    @Override public String toString() {
      return buffer.toString();
    }
  }
}
