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.FacesRenderer;
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

@FacesRenderer(componentFamily="javax.faces.Command", 
   rendererType="com.corejsf.TabbedPane")
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, chyba że przykryjemy metodę getRendersChildren() wersją zwracającą wartość true.

   public boolean getRendersChildren() {
      return true;
   }

   // Metoda decode odczytuje wartość parametru żądania, którego nazwa odpowiada identyfikatorowi
   // komponentu tabbedpane stosowanemu po stronie klienta. Wspomniany parametr żądania jest
   // kodowany w formie pola ukrytego przez metodę encodeHiddenField (wywoływaną przez metodę
   // encodeEnd). Wartość tego parametru jest ustawiana przez kod JavaScriptu wygenerowany
   // przez metodę encodeTab. W roli tej wartości stosuje się nazwę facety lub strony JSF.

   // Metoda decode używa wartości parametru żądania do ustawienia atrybutu content
   // komponentu tabbedpane.
   // I wreszcie metoda decode() kolejkuje zdarzenie akcji kierowane następnie (w fazie
   // wywoływania aplikacji cyklu życia JSF) do zarejestrowanych mechanizmów
   // nasłuchujących. Wspomniane mechanizmy można wskazywać albo za pomocą
   // atrybutu actionListener znacznika <corejsf:tabbedpane>, albo za pomocą 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
   // z klasą CSS wskazaną przez atrybut styleClass znacznika <corejsf:tabbedpane>
   // (jeśli ten znacznik 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"); // w ten sposób poprawiamy czytelność generowanego kodu HTML-a
   }

   // Metoda encodeChildren() jest wywoływana przez implementację JSF po metodzie encodeBegin().
   // Komponenty potomne komponentu <corejsf:tabbedpane> mają postać komponentów typu UISelectItem
   // ustawionych za pomocą 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 {
      // Ta metoda jest wywoływana, nawet jeśli komponent tabbedpane nie zawiera żadnych
      // 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"); // poprawia czytelność generowanego kodu HTML
   }

   // Metoda encodeEnd() jest wywoływana przez implementację JSF po metodzie encodeChildren().
   // Metoda encodeEnd() zapisuje ciało tabeli i koduje treść panelu podzielonego na zakładki
   // w jednym wierszu.

   // Treść panelu podzielonego na zakładki można określić albo w formie adresu URL strony JSF,
   // albo w formie nazwy facety, zatem metoda encodeEnd() sprawdza, czy ma do czynienia z facetą;
   // jeśli tak, metoda koduje tę facetę; w przeciwnym razie metoda dołącza odpowiednią stronę JSF.

   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().
   // Wyjaśnienie znaczenia pola ukrytego i jego wartości można znaleźć w metodzie decode.

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

   // Metoda encodeTab (wywoływana przez metodę encodeChildren) koduje element kotwicy
   // języka HTML z atrybutem onclick ustawiającym wartość pola ukrytego (zakodowanego przez
   // metodę encodeHiddenField) i wysyła formularz zawierający komponent tabbedpane. Więcej
   // informacji na temat tego pola ukrytego można znaleźć w metodzie decode.
   // Metoda encodeTab dodatkowo zapisuje atrybut klasy dla każdej zakładki odpowiadający albo
   // atrybutowi tabClass (w przypadku niezaznaczonych zakładek), albo atrybutowi
   // selectedTabClass (w przypadku zaznaczonej zakładki).

   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 komponentu tabbedpane klienta

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

                  // wysyła formularz zawierający komponent
                  "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"); // to make generated HTML easier to read
   }

   // Tekst zakładek komponentu tabbedpane można wskazać w formie
   // klucza w pakiecie zasobów lub bezpośrednio w formie tekstu wyświetlanego
   // w zakładce. Na podstawie tego tekstu metoda getLocalizedTabText próbuje
   // uzyskać wartość z pakietu komunikatów wskazanego przy użyciu atrybutu
   // resourceBundle znacznika <corejsf:tabbedpane>. W razie braku wartości
   // 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;
      // Parametr key w rzeczywistości nie reprezentuje klucza w pakiecie komunikatów,
      // zatem metoda zwraca łańcuch w niezmienionej formie.
      return localizedText;
   }

   // Metoda includePage używa mechanizmu kierowania żądań do serwletów, aby dołączyć
   // stronę właściwą wybranej zakładce.

   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, "Nie można załadować strony: " + content, ex);
      } catch (IOException ex) {
         logger.log(Level.WARNING, "Nie można załadować stronye: " + content, ex);
      }
   }
}
