package com.corejsf;

import java.io.IOException;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.faces.component.UIComponent;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.ActionEvent;
import javax.faces.model.SelectItem;
import javax.faces.render.Renderer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

// Klasa odpowiedzialna za wizualizację komponentu UITabbedPane

public class TabbedPaneRenderer extends Renderer {
   private static Logger logger = Logger.getLogger("com.corejsf.util");

   // Metoda getRendersChildren() domyślnie zwraca wartość false, zatem metoda encodeChildren() nie zostanie
   // wywołana, dopóki nie nadpiszemy metody getRendersChildren() wersją zwracającą wartość true.

   public boolean getRendersChildren() {
      return true;
   }

   // Metoda decode uzyskuje wartość parametru żądania, którego nazwa odpowiada identyfikatorowi
   // klienta komponentu panelu podzielonego na zakładki. Odpowiedni parametr żądania jest kodowany
   // przez metodę encodeHiddenField (wywoływaną przez metodę encodeEnd) w formie pola ukrytego.
   // Wartość tego parametru jest ustawiana przez skrypt języka JavaScript wygenerowany przez metodę
   // encodeTab i reprezentuje nazwę facety lub strony JSP.

   // Metoda decode wykorzystuje wartość tego parametru żądania do ustawienia
   // atrybutu treści komponentu panelu podzielonego na zakładki.
   // I wreszcie metoda decode() umieszcza zdarzenie akcji w kolejce, z której zostanie
   // przekazane metodom nasłuchującym w fazie wywołania aplikacji cyklu życia JSF.
   // Metody nasłuchujące akcji można definiować za pośrednictwem atrybutu
   // actionListener znacznika <corejsf:tabbedpane> lub znaczników <f:actionListener>
   // w ciele znacznika <corejsf:tabbedpane>.

   public void decode(FacesContext context, UIComponent component) {
      Map<String, String> requestParams 
         = context.getExternalContext().getRequestParameterMap();
      String clientId = component.getClientId(context);

      String content = (String) (requestParams.get(clientId));
      if (content != null && !content.equals("")) {
         UITabbedPane tabbedPane = (UITabbedPane) component;
         tabbedPane.setContent(content);
      }
      
      component.queueEvent(new ActionEvent(component));
   }

   // Metoda encodeBegin zapisuje początkowy element <table> języka HTML
   // wraz z klasą CSS wskazywaną za pośrednictwem atrybutu styleClass
   // znacznika <corejsf:tabbedpane> (jeśli taki atrybut w ogóle zdefiniowano).

   public void encodeBegin(FacesContext context, UIComponent component)
         throws java.io.IOException {
      ResponseWriter writer = context.getResponseWriter();
      writer.startElement("table", component);

      String styleClass = (String) component.getAttributes().get("styleClass");
      if (styleClass != null)
         writer.writeAttribute("class", styleClass, null);

      writer.write("\n"); // taki krok poprawia czytelność generowanego kodu HTML-a
   }

  // Metoda encodeChildren() jest wywoływana przez implementację JSF po metodzie encodeBegin().
  // Elementy potomne względem komponentu <corejsf:tabbedpane> mają postać komponentów UISelectItem
  // ustawionych za pośrednictwem jednego lub wielu znaczników <f:selectItem> bądź pojedynczego znacznika
  // <f:selectItems> w ciele znacznika <corejsf:tabbedpane>.

   public void encodeChildren(FacesContext context, UIComponent component)
         throws java.io.IOException {
      // Metoda encodeChildren jest wywoływana, nawet jeśli komponent tabbedpane
      // nie obejmuje komponentów potomnych.
      if (component.getChildCount() == 0) {
         return;
      }

      ResponseWriter writer = context.getResponseWriter();
      writer.startElement("thead", component);
      writer.startElement("tr", component);
      writer.startElement("th", component);

      writer.startElement("table", component);
      writer.startElement("tbody", component);
      writer.startElement("tr", component);

      for (SelectItem item : com.corejsf.util.Renderers.getSelectItems(component))
         encodeTab(context, writer, item, component);
      
      writer.endElement("tr");
      writer.endElement("tbody");
      writer.endElement("table");

      writer.endElement("th");
      writer.endElement("tr");
      writer.endElement("thead");
      writer.write("\n"); // taki krok poprawia czytelność generowanego kodu HTML-a
   }

   // Metoda encodeEnd() jest wywoływana przez implementację JSF po metodzie encodeChildren().
   // Metoda encodeEnd() zapisuje ciało tabeli i koduje zawartość komponentu tabbedpane
   // w ramach pojedynczego wiersza tej tabeli.

   // Treść panelu podzielonego na zakładki określamy albo w formie adresu URL
   // wskazującego na stronę JSP, albo w formie nazwy facety. Metoda encodeEnd()
   // sprawdza, czy wskazano facetę; jeśli tak, koduje ją; jeśli nie, dołącza stronę JSP.

   public void encodeEnd(FacesContext context, UIComponent component)
         throws java.io.IOException {
      ResponseWriter writer = context.getResponseWriter();
      UITabbedPane tabbedPane = (UITabbedPane) component;
      String content = tabbedPane.getContent();

      writer.startElement("tbody", component);
      writer.startElement("tr", component);
      writer.startElement("td", component);

      if (content != null) {
         UIComponent facet = component.getFacet(content);
         if (facet != null) {
            if (facet.isRendered()) {
               facet.encodeBegin(context);
               if (facet.getRendersChildren())
                  facet.encodeChildren(context);
               facet.encodeEnd(context);
            }
         } else
            includePage(context, component);
      }

      writer.endElement("td");
      writer.endElement("tr");
      writer.endElement("tbody");

      // Zamyka elementy kolumny, wiersza i tabeli.
      writer.endElement("table");

      encodeHiddenField(context, writer, component);
   }

   // Metoda encodeHiddenField jest wywoływana na końcu metody encodeEnd(). Znaczenie
   // pola ukrytego i jego wartości wyjaśniono przy okazji omawiania metody decode.

   private void encodeHiddenField(FacesContext context, ResponseWriter writer,
         UIComponent component) throws java.io.IOException {
      // Zapisuje pole ukryte, którego nazwa odpowiada identyfikatorowi klienta.
      writer.startElement("input", component);
      writer.writeAttribute("type", "hidden", null);
      writer.writeAttribute("name", component.getClientId(context), null);
      writer.endElement("input");
   }

   // Metoda encodeTab (wywoływana z poziomu metody encodeChildren) koduje element kotwicy języka
   // HTML z atrybutem onclick, który wymusza ustawianie wartości pola ukrytego zakodowanego przez
   // metodę encodeHiddenField i wysłanie na serwer całego formularza. Więcej informacji na temat
   // samego pola ukrytego można znaleźć w komentarzu poświęconym metodzie decode. Metoda encodeTab
   // dodatkowo zapisuje atrybut class dla poszczególnych zakładek; wartość tego atrybutu wskazuje albo
   // na atrybut tabClass (w przypadku niezaznaczonych zakładek), albo na atrybut selectedTabClass
   // (w przypadku zakładki zaznaczonej).

   private void encodeTab(FacesContext context, ResponseWriter writer,
         SelectItem item, UIComponent component) throws java.io.IOException {
      String tabText = getLocalizedTabText(component, item.getLabel());
      String content = (String) item.getValue();

      writer.startElement("td", component);
      writer.startElement("a", component);
      writer.writeAttribute("href", "#", "href");

      String clientId = component.getClientId(context);
      String formId = com.corejsf.util.Renderers.getFormId(context, component);

      writer.writeAttribute("onclick",
      // Zapisuje wartość pola ukrytego, którego nazwa odpowiada identyfikatorowi klienta.

            "document.forms['" + formId + "']['" + clientId + "'].value='"
                  + content + "'; " +

                  // Wysyła formularz, do którego należy komponent panelu.
                  "document.forms['" + formId + "'].submit(); ", null);

      UITabbedPane tabbedPane = (UITabbedPane) component;
      String selectedContent = tabbedPane.getContent(); 

      String tabClass = null;
      if (content.equals(selectedContent))
         tabClass = (String) component.getAttributes().get("selectedTabClass");
      else
         tabClass = (String) component.getAttributes().get("tabClass");

      if (tabClass != null)
         writer.writeAttribute("class", tabClass, null);

      writer.write(tabText);

      writer.endElement("a");
      writer.endElement("td");
      writer.write("\n"); // taki krok poprawia czytelność generowanego kodu HTML-a
   }

   // Tekst właściwy dla zakładek naszego komponentu panelu możemy zdefiniować albo z wykorzystaniem
   // kluczy pakietu komunikatów, albo bezpośrednio w formie tekstu wyświetlanego w zakładce. Na podstawie
   // tego tekstu metoda getLocalizedTabText próbuje odnaleźć odpowiednią wartość w pakiecie komunikatów
   // wskazanym za pośrednictwem atrybutu resourceBundle znacznika <corejsf:tabbedpane>. Jeśli próba
   // odnalezienia tej wartości zakończy się niepowodzeniem, metoda getLocalizedTabText zwraca łańcuch,
   // który przekazano na jej wejściu.

   private String getLocalizedTabText(UIComponent tabbedPane, String key) {
      String bundle = (String) tabbedPane.getAttributes().get("resourceBundle");
      String localizedText = null;

      if (bundle != null) {
         localizedText = com.corejsf.util.Messages.getString(bundle, key, null);
      }
      if (localizedText == null)
         localizedText = key;
      // Jeśli parametr key nie reprezentuje istniejącego klucza w pakiecie komunikatów,
      // zwracamy łańcuch wejściowy w niezmienionej formie.
      return localizedText;
   }

   // Metoda includePage wykorzystuje mechanizm rozdziału żądań serwletu do
   // dołączania strony właściwej dla wybranej zakładki.

   private void includePage(FacesContext fc, UIComponent component) {
	  ExternalContext ec = fc.getExternalContext();
      ServletContext sc = (ServletContext) ec.getContext();
      UITabbedPane tabbedPane = (UITabbedPane) component;
      String content = tabbedPane.getContent();
      
      ServletRequest request = (ServletRequest) ec.getRequest();
      ServletResponse response = (ServletResponse) ec.getResponse();
      try {
         sc.getRequestDispatcher(content).include(request, response);
      } catch (ServletException ex) {
         logger.log(Level.WARNING, "Couldn't load page: " + content, ex);
      } catch (IOException ex) {
         logger.log(Level.WARNING, "Couldn't load page: " + content, ex);
      }
   }
}
