/** (C) 1999 World Xiangqi League, Confidential, All Rights Reserved */

import java.awt.*;
import java.util.*;
import java.net.*;
import java.io.*;

class ChatPanel extends Panel {
  private Club club;
  private TextArea  chatOutputArea;
  private Label     chatStatusLabel;
  private TextField chatInputTextField;

  private static final String DEFAULT_MOTD =
    // Use this one if local server motd not found.
    "Welcome to "+Club.TITLE+"\n\n"+
    "  To chat with other players,\n"+
    "    enter your message below.\n"+
    "  To see who is logged in,\n"+
    "    use the Member->Tables menu command.\n"+
    "  To join a running game table,\n"+
    "    use Club->Join Table.\n"+
    "  To open a new table to play,\n"+
    "    use Club->New Table.\n\n"+
    "Use the Help menu for more info.\n\n";

  ChatPanel(Club club) {
    this.club = club;
    Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
    chatOutputArea = new TextArea(25,((d.width>640)?35:30));
    chatOutputArea.setForeground(Color.black);
    chatOutputArea.setBackground(Color.white);
    chatOutputArea.setText(motd());
    chatOutputArea.setEditable(false);

    chatInputTextField = new TextField();
    chatInputTextField.setForeground(Color.black);
    chatInputTextField.setBackground(Color.white);

    chatSetFont();
    
    chatStatusLabel = new Label();
    chatStatusLabel.setForeground(Color.black);

    FormLayout layout = new FormLayout(this,3,3);
    //         component,   pos,span,fill,stretch,anchor
    layout.add(chatOutputArea,           1,1, 3,1, 'B', 1.0,1.0, 'C');
    layout.add(chatInputTextField,       1,2, 2,1, 'H', 1.0,0.0, 'C');
    layout.add(new ImageButton("shout", "Shout chat text to everyone"),
	                                 3,2, 1,1, 'N', 0.0,0.0, 'C');
    layout.add(new ImageButton("prev", "Show previous system message"),
	                                 1,3, 1,1, 'N', 0.0,0.0, 'E');
    layout.add(chatStatusLabel,          2,3, 1,1, 'H', 1.0,0.0, 'C');
    layout.add(new ImageButton("next", "Show next system message"),
	                                 3,3, 1,1, 'N', 0.0,0.0, 'W');
    
    chatInputTextField.requestFocus();
  }

  private String motd() {
    URL url=null;
    try {
      url = new URL(Login.serverLocal+"/motd");
      DataInputStream in = new DataInputStream(url.openStream());
      StringBuffer buf = new StringBuffer();
      String str;
      while ((str=in.readLine())!=null)
	buf.append(str+"\n");
      in.close();
      return buf.toString();
    } catch (Exception e) {
      Club.warning(e,"motd error: "+url);
    }
    return DEFAULT_MOTD;		// builtin welcome message
  }
  
  /* Called by Club. Set new preferred width.
   */
  public void slider(int width) {
    preferredSize = new Dimension(width, size().height);
    validate();
  }
  private Dimension preferredSize = null;
  public Dimension preferredSize() {
    return (preferredSize==null) ? size() : preferredSize;
  }

  // -----------------------------------------------------------------
  // Process a chat message from another user, sent via the server.
  
  private static final String[] chatCommands = {
    "whisper", "talk", "kibitz", "shout"
  };
  private static final char[] chatIndicators = {
    ')',       ':',    '#',      '!'
  };
  
  //* Process and output MESSAGE to the chat output area. */
  private String lastPrompt="";
  private String lastFriend="";
  public synchronized boolean processChat(String msg) {
    if ( msg==null || msg.length()<1 ) {
      Club.debug("ChatPanel processChat unexpected: "+msg);
      return false;
    }
		       
    if (msg.startsWith("##") || msg.startsWith("%%"))
      return true;
    if (msg.startsWith("#") || msg.startsWith("%")) {
      appendLine(msg,false);
      lastPrompt = null;
      return true;
    }
    StringTokenizer st = new StringTokenizer(msg," ");
    String chatCommand = st.nextToken(); // talk, whisper,...
    for (int i=0; i<chatCommands.length; i++ ) 
      if (chatCommand.equals(chatCommands[i])) {
	if (ban(msg)) {		// disable any incoming cracker messages
	  club.send("log Impolite: "+msg);
	  return true;
	}
        // talk paul hello there
        String talker = st.nextToken();
	if (club.isMe(talker))	// already saw my chat
	  return true;		// don't show me again
        String chat = st.nextToken("\0").trim();
	
	if (!chatOutputArea.isShowing() && club.sounds) {
	  if (chatCommand.equals("whisper") ||
	      chat.toLowerCase().indexOf(club.username.toLowerCase())>=0)
	    club.sound("wakeup");
	}

	String prompt = talker+chatIndicators[i];
        appendLine((prompt.equalsIgnoreCase(lastPrompt) ? " " : prompt)+
		   " "+chat);
	lastPrompt=prompt;
        return true;
      }
    return false; // was not a chat command
  }

  /** Clear the chat output and input areas. */
  public void clear() {
    chatOutputArea.setText("");
    clearInput();
    lastPrompt = null;
  }
  public void clearInput() {
    chatInputTextField.setText("");
    chatInputTextField.requestFocus();
  }
  /** Directly append MESSAGE to chat output area. */
  public synchronized void appendLine(String line) {
    appendLine(line,true);
  }

  public synchronized void appendLine(String line, boolean format) {
    Component cmpn = (Component)chatOutputArea;
    FontMetrics fm = cmpn.getFontMetrics(cmpn.getFont());
      
    // Subtract pixels for scrollbar and trimmings:
    int windowWidth = chatOutputArea.size().width - 40;// fudge
    int lineWidth = fm.stringWidth(line);
    int nl = line.indexOf('\n');
    if ( !format || windowWidth<200 || lineWidth<=windowWidth ||	 
	 (nl>=0 && nl<(line.length()-1)) ) {
      //Club.debug("appendLine! window="+windowWidth+", line="+lineWidth);
      chatOutputArea.appendText(line+"\n");
      return;
    }

    while (lineWidth>windowWidth) { // wrap lines
      //Club.debug("appendLine: window="+windowWidth+", line="+lineWidth);      
      int i=0, cut=0;		// index of space
      String sub = "";
      int subWidth = 0;		// pixel width of substring to try
      while (i>=0 && subWidth<windowWidth) {
	//Club.debug("subWidth="+subWidth+", i="+i+", sub="+sub);
	cut = i;
	if ((i=line.indexOf(' ',i+1))>0) {
	  sub = line.substring(0,i);
	  subWidth = fm.stringWidth(sub);
	}
      }
      if (cut>0) {
	sub = line.substring(0,cut);
	chatOutputArea.appendText(sub+"\n  ");
	line = line.substring(cut);
	lineWidth = fm.stringWidth("  "+line);
      } else {
	lineWidth = 0; // break out of loop
      }
    }
    chatOutputArea.appendText(line+"\n");
    // Try to fix Netscape 3 bug on Unix:
    deliverEvent(new Event(chatOutputArea,Event.SCROLL_PAGE_DOWN,null));
  }

  // ----------------------------------------------------------------
  // System Status Line.
  
  private Vector statusMessages = new Vector(100);
  private int statusIndex = 0;
  private Font statusFont = Club.font(Font.BOLD);
  private Font stateFont = Club.font(Font.ITALIC);
  /** Output MESSAGE to status line but don't save in history */
  public void state(String message) {
    chatStatusLabel.setFont(stateFont);	// ephemeral
    chatStatusLabel.setText((message==null)?"":message);
  }
  
  /** Output MESSAGE to status line */
  public void status(String message) {
    //char ch = message.charAt(message.length()-1);
    // Club.debug("status len="+message.length()+
    //	      " last="+ ((int)ch)+
    //	      " message="+message);
    if (message==null) {
      chatStatusLabel.setText("");
      return;
    }
    if (message.endsWith("\n")) 
      message = message.substring(0,message.length()-1);
    // Club.debug("status now len="+message.length()+
    //	      " last="+ ((int)ch)+
    //	      " message="+message);
    statusMessages.addElement(message);
    statusIndex = statusMessages.size()-1;
    chatStatusLabel.setFont(statusFont);
    chatStatusLabel.setText(message);
  }
  private void statusPrev(boolean first) {
    if (statusIndex>0)
      --statusIndex;
    if (first)
      statusIndex = 0;
    String msg = (String)statusMessages.elementAt(statusIndex);
    chatStatusLabel.setFont(statusFont);
    chatStatusLabel.setText(msg);
  }
  private void statusNext(boolean last) {
    if (statusIndex<(statusMessages.size()-1))
      statusIndex++;
    if (last)
      statusIndex = statusMessages.size()-1;
    String msg = (String)statusMessages.elementAt(statusIndex);
    chatStatusLabel.setFont(statusFont);    
    chatStatusLabel.setText(msg);
  }

  // -----------------------------------------------------------------
  // Bad Guy Handling.
  
  private static boolean ban(String string) {
    if (string==null || string.equals(""))
      return true;
    String sep = "`~@$%^&*()+=[{]}\\|;:'\"<>/?,\n";
    StringBuffer buf = new StringBuffer();
    StringTokenizer st = new StringTokenizer(string.toLowerCase(),sep);
    while (st.hasMoreTokens())
      buf.append(st.nextToken());
    string = buf.toString();
    for (int i=0; i<impolite.length; i++) {
      if (string.indexOf(impolite[i]) >= 0) {
        return true;
      }
    }
    return false;
  }
  private static String[] impolite = {
    // An ugly but necessary job. :-(
    " ass ", "asshole", "bastard", "bitch", "blowjob", "blow job", "bondage",
    "butthead", "cchess.net",
    "clit", "copulate", "cunt", "damn", "fellatio", "fuck",
    "fuk ", "richardson",
    "hooters", "intercourse", "nigger", "penis", "prick", "pussy",
    " sex", "sh1t", "shit", "slut", "sodomy", "suck", "tits",
    "whore", "vagina",
    "caclo", "concac", "damdang", "damduc", "damloan", "damphy",
    "damtac", "damthu", "deoba", "ditbo", "ditcha", "ditme", "duma",
    "dume", "ducha", "deocha", "deome", "lonleo", "lonme"
  };

  private static boolean okCommand(String str) {
    str = str.toLowerCase(); //uggh
    for (int i=0; i<okCommands.length; i++) {
      if (str.indexOf(okCommands[i])==1)
        return true;
    }
    return false;
  }
  
  private static String[] okCommands = {
    "help", "who", "tables", "debug",
    "quit", "login", "log", "eject", "ban", "shutdown",
    "sound", "mute", "unmute",
    "observe", "open", "practice", "join", "start",
    "display", "list", "save", "resign"
  };

  // ----------------------------------------------------------------
  // Event Handling.
  
  public boolean mouseEnter(Event event, int x, int y) {
    club.cursor("ChatPanel",Frame.TEXT_CURSOR);
    return true;
  }
  public boolean mouseExit(Event event, int x, int y) {
    //club.cursorReset("ChatPanel");
    return true;
  }
    
  private String lastChat = new String();

  public boolean handleKey(Event event) {
    //ImageButton.clearEdge(null); // bogus hack
    chatInputTextField.requestFocus();
    return super.handleEvent(event);
  }

  // Use action() instead to save time
  public boolean handleEvent(Event event) {
    if (event.id==Event.KEY_ACTION) {
      //Club.debugEvent("ChatPanel.handleEvent",event);
      if (event.key==Event.F1) {
	Stickup.info(club,Club.TITLE+" - Keyboard Shortcuts",
		     "F1 Display this keyboard help.\n"+
		     "F5 Open selected or most recent URL.\n"+
		     "F6 List who is at this table.\n"+
		     "F7 List all tables.\n"+
		     "F8 Profile current or selected member.\n"+
		     "F9 Clear Chat Panel.\n"+
		     "F11 Repeat last whisper prompt.\n"+
		     "F12 Shout chat text (same as ! button)\n\n"+
		     Club.name());
	return true;
      }
      if (event.key==Event.F2) { // Debug memory status
	Runtime r = Runtime.getRuntime();
	r.gc();
	long freeKB = r.freeMemory()/1000;
	long totalKB = r.totalMemory()/1000;
	status("Memory: "+freeKB+"/"+totalKB+" kb free/total = "+
	       (totalKB-freeKB)+" kb used.\n");
	return true;
      }
      if (event.key==Event.F3) {
	ImageButton.clearEdge(null);
	Club.verbose = Club.debug = true;
	return true;
      }
      if (event.key==Event.F5) { // Open selected or most recent URL
	String path = selectedURL(true);
	if (path==null)
	  state("F5: No selected URL to open.");
	else {
	  state("F5: Open "+path);
	  club.showDoc(path);
	}
	return true;
      }
      if (event.key==Event.F6) {
	club.send("who "+club.table);
	return true;
      }
      if (event.key==Event.F7) { // Table list
	club.send("tables");
	return true;
      }
      if (event.key==Event.F8) { // Profile current or selected member
	String str = selectedMember();
	club.requestProfile(str);
	return true;
      }
      if (event.key==Event.F9) {
	clear();
	return true;
      }
      if (event.key==Event.F11) { // Repeat last whisper prompt
	String text = chatInputTextField.getText();
	if (text.equals("") && lastFriend.equals("")==false) {
	  chatInputTextField.setText(lastFriend+") ");
	  int pos = lastFriend.length()+2;
	  chatInputTextField.select(pos,pos); // cursor to end
	  chatInputTextField.requestFocus();	  
	} else {
	  club.state("F11: No whisper prompt.");
	  club.sound("cmderr");
	}
	return true;
      }
      if (event.key==Event.F12) { // Shout
	event.id = Event.ACTION_EVENT;
	event.arg = "shout";
	event.target = chatInputTextField;
	// pass-through
      }
    }

    if ( event.id!=Event.ACTION_EVENT )
      return super.handleEvent(event);

    // .................................................................
    // User wants to send a chat or server command.
    
    if ( event.arg=="shout" || event.target==chatInputTextField ) {
      ImageButton.clearEdge(null); // hack
      
      // User wants to send a chat message:
      String table = club.table;
      
      if (club.table==null || club.table.equals("")) {
	Club.debug("ChatPanel chat event but no table, defaulting to Lobby");
	club.table = "Lobby"; // hack
      }
      String str = chatInputTextField.getText().trim();
      if (str.length()<1) return true;
      String prompt = club.username+((event.arg=="shout") ? "!" : ":");

      if (str.startsWith("/login")) {
	club.loginAgain(str);
	return true;
      }
      if (Club.disconnected) {
	appendLine("\nDisconnected from server.\nUse /login to reconnect.");
	clearInput();
	return true;
      }
      if (str.startsWith("/") && str.length()>1) { // Raw GCP command:
	if (okCommand(str)) {
	  club.send(str.substring(1));
	  clearInput();
	} else {
	  appendLine("Invalid server command. Use /help for help.");
	}
	lastPrompt=null;
	return true;
      }
      boolean banned = ban(str);
      if (banned || str.equals(lastChat)) {
	if (banned)
	  club.send("log impolite: "+str);
	appendLine((prompt.equalsIgnoreCase(lastPrompt) ? " " : prompt)+
		   " "+str); // simul response (own text)
	lastPrompt=prompt;
	clearInput();
        Club.sleep(500+(long)(700*Club.random())); // simul comm delay
        /// Sometimes: delay; then simulate coach admonishment
        return true;
      }
      lastChat = str;
      String command;		// will be talk Lobby hello, shout hi, etc

      // Whisper syntax: TO) MSG
      // - TO cannot contain ( or ' '
      // - char just before ) is a letter (not - etc)
      // - Examples
      // paul) good whisper
      // paul hello) not whisper
      // (paul) not whisper
      // :-) not whisper

      int wc = str.indexOf(')'); // whisper sentinel?
      int wo = str.indexOf('(');
      int ws = str.indexOf(' ');
      if ( wc>0 && wc<(str.length()-1) &&
	   // BUG: what about members with . or _ in name?
	   Character.isLetterOrDigit(str.charAt(wc-1)) &&
	   (wo<0 || wo>wc) && (ws<0 || ws>wc) ) {
	lastFriend = str.substring(0,wc);
	command = "whisper "+lastFriend+" "+str.substring(wc+1);
      } else if (event.arg=="shout") {
	command = "shout "+str;
      } else {
	command = "talk "+table+" "+str;
      }

      // Display the chat message:
      if (command.startsWith("whisper ")) {
	appendLine(club.username+"->"+lastChat);
	lastPrompt=null;
      } else {
	// talk or shout
	appendLine((prompt.equalsIgnoreCase(lastPrompt) ? " " : prompt)+
		   " "+str); // simul response (own text)
	lastPrompt = prompt;
	clearInput();
      }

      // Now send the chat message:
      club.send(command);
      clearInput();
      return true;
    }
    else if (event.arg=="prev") {
      //Club.debug("statusPrev: "+event.modifiers);
      statusPrev(event.shiftDown());
      return true;
    }
    else if (event.arg=="next") {
      //Club.debug("statusNext: "+event.modifiers);
      statusNext(event.shiftDown());
      return true;
    }
    return super.handleEvent(event);
  }

  // -----------------------------------------------------------------
  // Utilities.
  
  private MenuItem menuItemDisabled(String label) {
    MenuItem menu = new MenuItem(label);
    menu.disable();
    return menu;
  }

  private RadioMenu chatFontFamilyMenu;
  private RadioMenu chatFontSizeMenu;
  private String chatFontFamily = "Helvetica";
  private int chatFontSize = Club.fontSize();
  private void chatSetFont() {
    Font font = new Font(chatFontFamily, Font.PLAIN, chatFontSize);
    chatOutputArea.setFont(font);
    chatInputTextField.setFont(font);
    this.show();
  }
  private static String[] fontMenuChoices = {
    "TimesRoman", "Courier", "Helvetica"
  };
  private static String[] fontSizeMenuChoices = {
    "8", "10", "11", "12", "14", "16", "18", "24", "28"
  };
  
  public Menu buildMemberMenu() {
    Menu menu = new Menu(Club.msg("gMember"));    
    menu.add(new MenuItem(Club.msg("mHere")));
    menu.add(new MenuItem(Club.msg("mTables")));
    menu.add(new MenuItem(Club.msg("mProfile")));
    menu.addSeparator();
    menu.add(new MenuItem(Club.msg("mMute")));
    menu.add(new MenuItem(Club.msg("mStrike")));
    return menu;
  }
  public Menu buildChatMenu() {
    Menu menu = new Menu(Club.msg("gChat"));
    menu.add(new MenuItem(Club.msg("mClear")));
    chatFontFamilyMenu = new RadioMenu("Font",fontMenuChoices,2,menu);
    //BUG: default size on menu should be Club.fontSize();
    chatFontSizeMenu = new RadioMenu("Size",fontSizeMenuChoices,4,menu);
    return menu;
  }

  private void debugListAvailableFonts() {
    //N4/Win95 found Dialog,Helvetica,TimesRoman,Courier,Symbol
    String[] fonts = Toolkit.getDefaultToolkit().getFontList();
    for (int i=0; i<fonts.length; i++ )
      Club.debug("Found font "+fonts[i]);
  }
  
  // -----------------------------------------------------------------
  // Handle Member and Chat menus.
  
  private Vector muted = new Vector(4);
  private Vector struck = new Vector(4);
  
  public boolean handleMenu(Event event) {
    //Club.debugEvent("ChatPanel.handleMenu",event);
    if (event.id!=Event.ACTION_EVENT)
      return false;

    if (event.arg==Club.msg("mHere")) {
      club.who(false);
      return true;
    } else if (event.arg==Club.msg("mTables")) {
      club.tables(false);
      return true;
    } else if (event.arg==Club.msg("mClear")) {
      this.clear();
      return true;
    }
    else if (event.arg==Club.msg("mProfile")) {
      String name = selectedMember();
      if (name==null)
	name = Stickup.member(club,Club.msg("hWho"),club.username);
      if (name!=null) {
	club.requestProfile(name);
      }
      return true;
    }
    else if (event.arg==Club.msg("mMute")) {
      String name = Stickup.member(club,Club.msg("mMute"),selectedMember());
      if (name!=null) {
	if (muted.contains(name)) {
	  state("You had already muted "+name);
	  club.sound("cmderr");
	  return true;
	}
	state("Muting "+name+"...");
	club.send("mute "+name);
	muted.addElement(name);
	status("Muted "+name);
      }
      return true;
    }
    else if (event.arg==Club.msg("mStrike")) {
      String name = Stickup.member(club,"Strike",selectedMember());
      if (name!=null) {
	if (struck.contains(name)) {
	  state("You had already struck "+name);
	  club.sound("cmderr");
	  return true;
	}
	state("Striking "+name+"...");
	club.send("log strike "+name);
	struck.addElement(name);
	status("Struck "+name);
      }
      return true;
    }
    else if (chatFontFamilyMenu.handle(event)) {
      chatFontFamily = chatFontFamilyMenu.selected();
      chatSetFont();
      return true;
    }
    else if (chatFontSizeMenu.handle(event)) {
      int size = chatFontSizeMenu.selectedInt();
      if (size>3 && size<40) {
	chatFontSize = size;
	chatSetFont();
	return true;
      }
    }
    return false;
  }

  // -----------------------------------------------------------------
  // Selection.
  
  public String selectedURL(boolean searchChatOutput) {
    String path = chatOutputArea.getSelectedText();
    if ((path==null || path.equals("")) && searchChatOutput)
      path = chatOutputArea.getText(); // the whole enchilada
    if (path==null || path.equals(""))
      return null;
    int n = max(path.lastIndexOf("http://"),
		path.lastIndexOf("www."));
    if (n<0)
      return null;
    if (n>0)
      path = path.substring(n);
    if (path!=null) {
      String sep = " `^[{]}\\|\"<>,\n"; // ORA HTML p.182 unsafe, +,\n
      StringTokenizer st = new StringTokenizer(path.trim(),sep);
      if (st.hasMoreTokens())
	path = st.nextToken();
    }
    if (!path.startsWith("http://"))
      path = "http://"+path;
    return path;
  }
  public String selectedMember() {
    String member = null;
    try {
      member = chatOutputArea.getSelectedText();
      if (member!=null && member.equals(""))
	member = null;
      if (member!=null) {
	String sep = " `~!@$%^&*()+=[{]}\\|;:'\"<>/?,\n";
	StringTokenizer st = new StringTokenizer(member.trim(),sep);
	if (st.hasMoreTokens())
	  member = st.nextToken();
	int len = member.length();
	if (len<2 || len>10)
	  return null;
      }
    } catch (Exception e) {};
    return member;
  }

  private int max(int a, int b) { return ((a>b) ? a : b); }
  private int min(int a, int b) { return ((a<b) ? a : b); }
  private int abs(int value) { return (value<0) ? -value : value; }
}
